@sema-agent/server 7.133.0 → 7.134.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/MIGRATION.md +87 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +129 -11
- package/deploy/sema-up/README.md +16 -0
- package/deploy/sema-up/chart/Chart.yaml +1 -1
- package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
- package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
- package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
- package/deploy/sema-up/chart/templates/server.yaml +14 -0
- package/deploy/sema-up/chart/values-full-single.yaml +5 -1
- package/deploy/sema-up/chart/values.yaml +34 -0
- package/dist/approval-ask-audit-store.d.ts +1 -1
- package/dist/approval-card.d.ts +4 -4
- package/dist/approval-card.js +1 -2
- package/dist/approval-content-kind.d.ts +5 -2
- package/dist/approval-content-kind.js +16 -3
- package/dist/approval-gate-currency.d.ts +72 -0
- package/dist/approval-gate-currency.js +94 -0
- package/dist/approval.d.ts +6 -0
- package/dist/approval.js +31 -14
- package/dist/boot/coordinators.d.ts +6 -1
- package/dist/boot/coordinators.js +6 -3
- package/dist/boot/leader.d.ts +2 -0
- package/dist/boot/leader.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -2
- package/dist/boot/runner-deps.d.ts +18 -5
- package/dist/boot/runner-deps.js +9 -0
- package/dist/boot/stage-06-runners.d.ts +4 -3
- package/dist/boot/stage-06-runners.js +15 -2
- package/dist/boot/stage-07-capability-layer.d.ts +2 -1
- package/dist/boot/stage-08-reapers.d.ts +2 -1
- package/dist/boot/stage-09-leader.d.ts +2 -1
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +2 -1
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/boot/stores.d.ts +8 -0
- package/dist/boot/stores.js +8 -12
- package/dist/boot/web-admin.d.ts +6 -0
- package/dist/boot/web-admin.js +11 -2
- package/dist/budget.js +5 -1
- package/dist/caller-text-echo.d.ts +2 -0
- package/dist/caller-text-echo.js +2 -1
- package/dist/canonical-json.d.ts +2 -41
- package/dist/canonical-json.js +2 -13
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/mcp-probe.js +2 -2
- package/dist/capability-facts.d.ts +2 -0
- package/dist/capability-facts.js +4 -4
- package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
- package/dist/checkpoint-ctx-seal-m28.js +19 -0
- package/dist/field-conflict.d.ts +2 -1
- package/dist/field-conflict.js +1 -0
- package/dist/file-history-disclosure.d.ts +34 -5
- package/dist/file-history-disclosure.js +2 -2
- package/dist/fleet/fleet-bus.d.ts +4 -0
- package/dist/fleet/fleet-bus.js +1 -0
- package/dist/hooks/hook-runner.js +2 -2
- package/dist/http/admission.js +8 -0
- package/dist/http/approval-sweeps.js +3 -1
- package/dist/http/checkpoint-ctx.d.ts +157 -2
- package/dist/http/checkpoint-ctx.js +158 -2
- package/dist/http/dispatch.js +3 -3
- package/dist/http/durable-run-leg.js +3 -2
- package/dist/http/idempotency.d.ts +2 -2
- package/dist/http/idempotency.js +2 -2
- package/dist/http/principal-gate.d.ts +32 -6
- package/dist/http/principal-gate.js +27 -4
- package/dist/http/refusal-body.d.ts +12 -0
- package/dist/http/refusal-body.js +11 -0
- package/dist/http/request-guards.d.ts +0 -9
- package/dist/http/request-guards.js +2 -16
- package/dist/http/resume-ctx-read.d.ts +48 -1
- package/dist/http/resume-ctx-read.js +26 -3
- package/dist/http/resume-legs.d.ts +2 -1
- package/dist/http/resume-legs.js +37 -22
- package/dist/http/route-ctx.d.ts +0 -9
- package/dist/http/routes/agents-roster.js +2 -3
- package/dist/http/routes/approvals-assistant.js +43 -9
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -3
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/fleet.js +4 -4
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +4 -4
- package/dist/http/routes/runs.js +30 -9
- package/dist/http/routes/session-sync.d.ts +3 -2
- package/dist/http/routes/session-sync.js +38 -20
- package/dist/http/routes/sessions-list.d.ts +1 -1
- package/dist/http/routes/sessions-list.js +3 -4
- package/dist/http/routes/sessions.js +4 -4
- package/dist/http/routes/tasks.js +21 -16
- package/dist/http/routes/trace-usage.js +7 -9
- package/dist/http/run-declared-families.d.ts +16 -0
- package/dist/http/run-declared-families.js +12 -0
- package/dist/http/server.d.ts +6 -0
- package/dist/http/server.js +5 -2
- package/dist/http/submit-ctx.d.ts +4 -3
- package/dist/http/submit-ctx.js +19 -3
- package/dist/http/wire-types.d.ts +7 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/leader/wire.d.ts +4 -0
- package/dist/leader/wire.js +8 -7
- package/dist/main.js +1 -1
- package/dist/memory-origin-fence.js +2 -2
- package/dist/model-compat.d.ts +1 -1
- package/dist/observability/prompt-manifest.d.ts +19 -8
- package/dist/observability/prompt-manifest.js +9 -8
- package/dist/observability/tool-trace.js +8 -9
- package/dist/orchestration/workflow-park-decision.d.ts +1 -1
- package/dist/orchestration/workflow-park-decision.js +1 -0
- package/dist/parked-decide.d.ts +4 -5
- package/dist/parked-decide.js +3 -1
- package/dist/permission-rule-vocab.d.ts +1 -23
- package/dist/permission-rule-vocab.js +0 -13
- package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
- package/dist/plugins/approval-payload-fingerprint.js +1 -1
- package/dist/plugins/caching-session-store.d.ts +1 -1
- package/dist/plugins/caching-session-store.js +4 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
- package/dist/plugins/checkpoint-store-sql.js +45 -1
- package/dist/plugins/local-checkpoint-store.d.ts +13 -2
- package/dist/plugins/local-checkpoint-store.js +26 -0
- package/dist/plugins/local-session-store.d.ts +12 -9
- package/dist/plugins/local-session-store.js +23 -14
- package/dist/plugins/local-session-window.d.ts +12 -6
- package/dist/plugins/local-session-window.js +1 -17
- package/dist/plugins/one-time-migrations.d.ts +11 -0
- package/dist/plugins/one-time-migrations.js +12 -0
- package/dist/plugins/permission-rule-store-file.js +10 -6
- package/dist/plugins/permission-rule-store-sql.js +8 -8
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +39 -17
- package/dist/plugins/remote-env-device.d.ts +14 -7
- package/dist/plugins/remote-env-device.js +23 -15
- package/dist/plugins/session-ownership.d.ts +11 -0
- package/dist/plugins/session-ownership.js +11 -0
- package/dist/plugins/session-placement.d.ts +12 -14
- package/dist/plugins/session-placement.js +2 -15
- package/dist/plugins/tidb-session-store.d.ts +1 -1
- package/dist/plugins/tidb-session-store.js +39 -17
- package/dist/request-tool-axes.d.ts +12 -0
- package/dist/request-tool-axes.js +20 -0
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +4 -3
- package/dist/sealed-key.d.ts +32 -0
- package/dist/sealed-key.js +55 -1
- package/dist/security.d.ts +9 -7
- package/dist/session-error-wire.d.ts +12 -5
- package/dist/session-error-wire.js +4 -1
- package/dist/session-sync-kernel.d.ts +37 -10
- package/dist/session-sync-kernel.js +10 -0
- package/dist/task-a2a.js +4 -3
- package/dist/task-mcp.d.ts +2 -2
- package/dist/task-mcp.js +20 -8
- package/dist/task-settings.d.ts +5 -1
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +151 -52
- package/dist/tool-approval.js +113 -20
- package/dist/tool-rule-names.d.ts +6 -0
- package/dist/trace/core-keyset-guard.d.ts +4 -3
- package/dist/trace/exit-seat.d.ts +13 -1
- package/dist/trace/exit-seat.js +55 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -4
- package/dist/trace/redact.d.ts +6 -3
- package/dist/trace/redact.js +191 -23
- package/dist/trace/self-minted-wire.d.ts +4 -0
- package/dist/trace/self-minted-wire.js +1 -0
- package/dist/trace/wire-projection-faces.d.ts +6 -0
- package/dist/trace/wire-projection-faces.js +6 -0
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/skills-mcp.js +2 -3
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +34 -2
- package/scripts/drain-check.mysql.sql +20 -2
- package/scripts/drain-check.pg.sql +20 -2
- package/dist/mcp-tool-faces.d.ts +0 -69
- package/dist/mcp-tool-faces.js +0 -104
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* 宿主可枚举、release 不真删的普通会话 ——「声明了 full tier 却在静默说谎」。⇒ 在**声明了分区**的
|
|
34
34
|
* 店上,带 placement 的 acquire 必须回读到**已持久化**且同一元组身份的行,回读缺失/异组=响亮拒。
|
|
35
35
|
*/
|
|
36
|
-
import { type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord } from "@sema-agent/core";
|
|
36
|
+
import { type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord, type SessionStore } from "@sema-agent/core";
|
|
37
37
|
/**
|
|
38
38
|
* 两支 SQL 孪生的 placement **声明**(core `SessionStore.placements`)。
|
|
39
39
|
*
|
|
@@ -88,20 +88,18 @@ export interface PlacementAcquireOpts {
|
|
|
88
88
|
placement?: SessionPlacement;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* 准入判据 ——
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `requireExisting`(读/复活形,义务 5)与「创建调用及其幂等重取」(带 placement 的可信内部链)
|
|
98
|
-
* 是仅有的两个合法形。placement 不可变意味着带 placement 的参数**永远重写不了**任何东西——持久化
|
|
99
|
-
* 的那一份恒赢,所以放行它不开任何口子。
|
|
100
|
-
* · 方向②(镜像/squat):带 placement 的创建形回读到**缺席**或**异组**元组 ⇒ 拒。缺席 = 并发的
|
|
101
|
-
* claim 形抢先建出了普通会话(或带外写把元组抹了);异组 = 同 id 上坐着另一条委派链的转录。
|
|
102
|
-
* 两者都不许让「声明了 durable 分区」这句话在静默中变成谎。
|
|
91
|
+
* 准入判据 —— 只有引擎那一只:core 包根导出 `admitSessionPlacement(sessionId, persisted, request, capability)`(core 7.54.0 #1468 K2;
|
|
92
|
+
* `TtlSessionStore` 自己调的同一只函数,两个方向:claim 关门 / 镜像·squat,`placement_refused`)。`capability.subagentPlacement` 读的是**店自己的声明**
|
|
93
|
+
* (`placements.subagent` 在场 —— 与 core `TtlSessionStore` 同一只读法,{@link placementCapabilityOf}),不是另写一个「这只店声明了」的常量:声明只有一处。
|
|
94
|
+
* 修前本模块持一份 SQL 镜像(`assertPlacementAdmission`,逐条同义、文案自铸)—— 与引擎是两份判据;随 7.54.0 退役、零别名。
|
|
95
|
+
* 拒句随之换成引擎的句子(缺元组那一形的结尾词:「or the repo dropped the tuple」,此前本仓写「written away out of band」)——
|
|
96
|
+
* 按原句匹配告警的运维规则要跟着改(MIGRATION 7.134.0 节记了这一句)。
|
|
103
97
|
*/
|
|
104
|
-
export declare function
|
|
98
|
+
export declare function placementCapabilityOf(store: {
|
|
99
|
+
readonly placements?: SessionStore["placements"];
|
|
100
|
+
}): {
|
|
101
|
+
readonly subagentPlacement: boolean;
|
|
102
|
+
};
|
|
105
103
|
/**
|
|
106
104
|
* 一次 `listPlaced` 最多带回的行数。
|
|
107
105
|
*
|
|
@@ -45,21 +45,8 @@ export function placementInsertValues(placement, placedAt) {
|
|
|
45
45
|
placedAt,
|
|
46
46
|
];
|
|
47
47
|
}
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
if (persisted !== undefined && opts?.placement === undefined) {
|
|
52
|
-
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${persisted.kind} placement partition — a claim-form acquire cannot attach to it (read it with requireExisting, or address the agent through its a* handle)`);
|
|
53
|
-
}
|
|
54
|
-
if (opts?.placement === undefined)
|
|
55
|
-
return;
|
|
56
|
-
const want = opts.placement;
|
|
57
|
-
if (persisted === undefined) {
|
|
58
|
-
throw new SessionError("placement_refused", `session ${sessionId} exists OUTSIDE the ${want.kind} placement partition it was declared into — refusing to run a subagent transcript on it (a concurrent claim-form create won this id, or the tuple was written away out of band)`);
|
|
59
|
-
}
|
|
60
|
-
if (persisted.kind !== want.kind || persisted.scope !== want.scope || persisted.handle !== want.handle) {
|
|
61
|
-
throw new SessionError("placement_refused", `session ${sessionId}'s persisted placement does not match the declared one (persisted ${persisted.kind}/${persisted.scope ?? "-"}/${persisted.handle ?? "-"} vs declared ${want.kind}/${want.scope ?? "-"}/${want.handle ?? "-"}) — refusing a cross-tuple attach`);
|
|
62
|
-
}
|
|
48
|
+
export function placementCapabilityOf(store) {
|
|
49
|
+
return { subagentPlacement: store.placements?.subagent !== undefined };
|
|
63
50
|
}
|
|
64
51
|
export const LIST_PLACED_BATCH = 1000;
|
|
65
52
|
//# sourceMappingURL=session-placement.js.map
|
|
@@ -187,7 +187,7 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
187
187
|
* We instead SELECT the entire `session_event` log ordered by seq — every durable entry, compaction floor
|
|
188
188
|
* bypassed — and re-seq it dense from 0 into the new session, ids/parents/payload byte-verbatim.
|
|
189
189
|
*/
|
|
190
|
-
fork(sourceId: string, owner
|
|
190
|
+
fork(sourceId: string, owner?: string | null): Promise<string | null>;
|
|
191
191
|
/**
|
|
192
192
|
* 2c session-sync — EXPORT a session's full durable entry log for cross-backend migration. This is the
|
|
193
193
|
* READ half of {@link fork}, surfaced as the core `SessionRepo.exportEntries` seam: the WHOLE `session_event` log,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry } from "@sema-agent/core";
|
|
1
|
+
import { StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, admitSessionPlacement } from "@sema-agent/core";
|
|
2
2
|
import { TiDBSessionStorage } from "./tidb-session-storage.js";
|
|
3
3
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
4
4
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
@@ -9,8 +9,9 @@ import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "
|
|
|
9
9
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
10
10
|
import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
|
|
11
11
|
import { createLogger } from "../observability/logger.js";
|
|
12
|
-
import {
|
|
13
|
-
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
12
|
+
import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
13
|
+
import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
14
|
+
import { assertFirstOwnerWritable, forkOwnerOf } from "./session-ownership.js";
|
|
14
15
|
export class TiDBSessionStore {
|
|
15
16
|
pool;
|
|
16
17
|
retention = MANAGED_RETENTION;
|
|
@@ -32,7 +33,7 @@ export class TiDBSessionStore {
|
|
|
32
33
|
const inflight = this.pending.get(sessionId);
|
|
33
34
|
if (inflight) {
|
|
34
35
|
const acquired = await inflight;
|
|
35
|
-
|
|
36
|
+
admitSessionPlacement(sessionId, await this.placementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
|
|
36
37
|
return acquired;
|
|
37
38
|
}
|
|
38
39
|
const p = this.load(sessionId, false, opts).finally(() => this.pending.delete(sessionId));
|
|
@@ -42,7 +43,7 @@ export class TiDBSessionStore {
|
|
|
42
43
|
async load(sessionId, requireExisting = false, opts) {
|
|
43
44
|
let storage = await TiDBSessionStorage.wake(this.pool, sessionId);
|
|
44
45
|
if (storage) {
|
|
45
|
-
|
|
46
|
+
admitSessionPlacement(sessionId, (await storage.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
46
47
|
return { session: new StoredSession(storage), sessionId };
|
|
47
48
|
}
|
|
48
49
|
if (requireExisting)
|
|
@@ -52,10 +53,15 @@ export class TiDBSessionStore {
|
|
|
52
53
|
if (!storage) {
|
|
53
54
|
throw new SessionError("storage", `session ${sessionId} vanished between register and wake (a concurrent purge won the race) — retry`);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
admitSessionPlacement(sessionId, (await storage.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
|
|
56
57
|
return { session: new StoredSession(storage), sessionId };
|
|
57
58
|
}
|
|
58
59
|
async register(sessionId, owner) {
|
|
60
|
+
if (typeof owner !== "string" && owner !== null) {
|
|
61
|
+
if ((await this.ownerOf(sessionId)) !== undefined)
|
|
62
|
+
return;
|
|
63
|
+
assertFirstOwnerWritable(sessionId, owner);
|
|
64
|
+
}
|
|
59
65
|
await this.insertMeta(sessionId, owner, undefined);
|
|
60
66
|
}
|
|
61
67
|
async createOnAcquire(sessionId, placement) {
|
|
@@ -259,10 +265,11 @@ export class TiDBSessionStore {
|
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
async fork(sourceId, owner) {
|
|
262
|
-
const [metaRows] = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = ?", [sourceId]);
|
|
268
|
+
const [metaRows] = await this.pool.query("SELECT leaf_id, owner FROM session_meta WHERE session_id = ?", [sourceId]);
|
|
263
269
|
if (!metaRows[0])
|
|
264
270
|
return null;
|
|
265
271
|
const leafId = metaRows[0].leaf_id ?? null;
|
|
272
|
+
const forkOwner = forkOwnerOf(owner, metaRows[0].owner ?? null) ?? null;
|
|
266
273
|
const [eventRows] = await this.pool.query(`SELECT entry_id, parent_id, type, ${jsonColumnText("tidb", "payload")}, ts FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [sourceId]);
|
|
267
274
|
const newId = uuidv7();
|
|
268
275
|
const now = new Date();
|
|
@@ -274,7 +281,7 @@ export class TiDBSessionStore {
|
|
|
274
281
|
const payload = String(r.payload);
|
|
275
282
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
276
283
|
}
|
|
277
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [newId,
|
|
284
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [newId, forkOwner, now, now, leafId, eventRows.length, mintSessionIncarnation()]);
|
|
278
285
|
await conn.commit();
|
|
279
286
|
return newId;
|
|
280
287
|
}
|
|
@@ -476,6 +483,23 @@ class TiDBStagingHandle {
|
|
|
476
483
|
const conn = await this.pool.getConnection();
|
|
477
484
|
try {
|
|
478
485
|
await conn.beginTransaction();
|
|
486
|
+
const now = new Date();
|
|
487
|
+
const minted = mintSessionIncarnation();
|
|
488
|
+
if (opts.expectedOwner !== undefined) {
|
|
489
|
+
const [cas] = await conn.query("UPDATE session_meta SET leaf_seq = leaf_seq + 1 WHERE session_id = ? AND owner <=> ?", [this.realSessionId, opts.expectedOwner]);
|
|
490
|
+
if (cas.affectedRows !== 1) {
|
|
491
|
+
await conn.rollback();
|
|
492
|
+
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,NULL,0,?) ON DUPLICATE KEY UPDATE session_id = session_id", [this.realSessionId, owner, now, now, minted]);
|
|
497
|
+
const [claimed] = await conn.query("SELECT incarnation FROM session_meta WHERE session_id = ? FOR UPDATE", [this.realSessionId]);
|
|
498
|
+
if (String(claimed[0]?.incarnation) !== minted) {
|
|
499
|
+
await conn.rollback();
|
|
500
|
+
throw new StagedCommitOwnerChangedError(this.realSessionId);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
479
503
|
const [stagedRows] = await conn.query("SELECT entry_id FROM session_event WHERE session_id = ? ORDER BY seq ASC FOR UPDATE", [this.stagingId]);
|
|
480
504
|
const [dstRows] = await conn.query("SELECT entry_id FROM session_event WHERE session_id = ? ORDER BY seq ASC FOR UPDATE", [this.realSessionId]);
|
|
481
505
|
const [dstMeta] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ?", [this.realSessionId]);
|
|
@@ -484,15 +508,15 @@ class TiDBStagingHandle {
|
|
|
484
508
|
throw new SessionError("placement_refused", `session ${this.realSessionId} belongs to the ${String(dstMeta[0].placement_kind)} placement partition — a session-sync staged commit cannot overwrite a delegation transcript`);
|
|
485
509
|
}
|
|
486
510
|
const srcIds = stagedRows.map((r) => String(r.entry_id));
|
|
487
|
-
const dstIds =
|
|
511
|
+
const dstIds = opts.expectedOwner !== undefined ? dstRows.map((r) => String(r.entry_id)) : null;
|
|
488
512
|
const rel = classifySyncRelationshipByIds(srcIds, dstIds);
|
|
489
|
-
if ((rel.relation === "fork" || rel.relation === "stale") && opts
|
|
513
|
+
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
490
514
|
await conn.rollback();
|
|
491
515
|
throw new SyncConflictError(this.realSessionId, rel);
|
|
492
516
|
}
|
|
493
|
-
if (rel.relation === "fast_forward" && opts
|
|
494
|
-
const [dstFull] = await conn.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [this.realSessionId]);
|
|
495
|
-
const [stagedFull] = await conn.query(`SELECT entry_id, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [this.stagingId]);
|
|
517
|
+
if (rel.relation === "fast_forward" && opts.resolution !== "overwrite-dst" && dstIds !== null && dstIds.length > 0) {
|
|
518
|
+
const [dstFull] = await conn.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC FOR UPDATE`, [this.realSessionId]);
|
|
519
|
+
const [stagedFull] = await conn.query(`SELECT entry_id, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC FOR UPDATE`, [this.stagingId]);
|
|
496
520
|
const dstSet = new Set(dstIds);
|
|
497
521
|
const dstEntries = dstFull.map((r) => decodeSessionEventPayload(r.payload, this.realSessionId, r.seq));
|
|
498
522
|
const stagedShared = stagedFull
|
|
@@ -506,13 +530,11 @@ class TiDBStagingHandle {
|
|
|
506
530
|
}
|
|
507
531
|
await conn.query("DELETE FROM session_event WHERE session_id = ?", [this.realSessionId]);
|
|
508
532
|
await conn.query("UPDATE session_event SET session_id = ? WHERE session_id = ?", [this.realSessionId, this.stagingId]);
|
|
509
|
-
const [lastRow] = await conn.query(`SELECT ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq DESC LIMIT 1`, [this.realSessionId]);
|
|
533
|
+
const [lastRow] = await conn.query(`SELECT ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq DESC LIMIT 1 FOR UPDATE`, [this.realSessionId]);
|
|
510
534
|
let leafId = null;
|
|
511
535
|
if (lastRow[0])
|
|
512
536
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRow[0].payload, this.realSessionId, "last staged row"));
|
|
513
|
-
|
|
514
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?) " +
|
|
515
|
-
"ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
|
|
537
|
+
await conn.query("UPDATE session_meta SET owner = ?, updated_at = ?, leaf_id = ?, leaf_seq = ? WHERE session_id = ?", [owner, now, leafId, srcIds.length, this.realSessionId]);
|
|
516
538
|
const [afterMeta] = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = ?", [this.realSessionId]);
|
|
517
539
|
if (afterMeta[0]?.placement_kind != null) {
|
|
518
540
|
await conn.rollback();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求侧 `toolAxes` 声明的读法 —— `mcpServers[].toolAxes`(`task-mcp.ts`)与 `a2aPeers[].toolAxes`(`task-a2a.ts`)同一只。
|
|
3
|
+
*
|
|
4
|
+
* 律(一句):判官是引擎包根的 `readToolAxes(declared, at)`;请求侧再多一条 —— **不许写 `boundaryFlow`**。
|
|
5
|
+
* `boundaryFlow: "engine"`(须同条 `egress: false`)声明一只引擎专用工具:名册行读成零条腿,设备车道的发射闸对它的调用不欠任何东西。
|
|
6
|
+
* 引擎分不出一条声明来自请求还是部署,所以这个位只许部署声明(`config.d` / 配置中心)写 —— 那两条腿原样交引擎(由引擎判 `egress: false`)。
|
|
7
|
+
* 读数形与引擎判官同一只(`ToolAxesRead`):请求侧写了这个位 ⇒ `ok: false`,原因句点名工具(或 skill)与出路;各腿按自己既有的「判不过」处置读它
|
|
8
|
+
* (MCP 新提交 400 点名 / 续跑重放整条 server 不进;A2A 整条对端不挂、warn 点名)。
|
|
9
|
+
*/
|
|
10
|
+
import { type ToolAxesRead, type ToolAxesSurface } from "@sema-agent/core";
|
|
11
|
+
export declare function readRequestToolAxes(declared: unknown, at: ToolAxesSurface): ToolAxesRead;
|
|
12
|
+
//# sourceMappingURL=request-tool-axes.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readToolAxes } from "@sema-agent/core";
|
|
2
|
+
import { stripControlAndBidi } from "@sema-agent/settings-schema";
|
|
3
|
+
import { MAX_KEY_ECHO } from "./request-key-closure.js";
|
|
4
|
+
const KEY_NOUN = { mcp: "tool", a2a: "skill" };
|
|
5
|
+
export function readRequestToolAxes(declared, at) {
|
|
6
|
+
const read = readToolAxes(declared, at);
|
|
7
|
+
if (!read.ok || read.toolAxes === undefined)
|
|
8
|
+
return read;
|
|
9
|
+
for (const [name, axes] of Object.entries(read.toolAxes)) {
|
|
10
|
+
if (axes !== undefined && Object.hasOwn(axes, "boundaryFlow")) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
problem: `the axes declared for ${KEY_NOUN[at]} ${JSON.stringify(stripControlAndBidi(name).slice(0, MAX_KEY_ECHO))} carry "boundaryFlow", which only a deployment's declaration may state ` +
|
|
14
|
+
`(it declares an engine-only ${KEY_NOUN[at]}, so the device lane's launch gate would owe its calls nothing; the engine cannot tell a request's entry from a deployment's) — drop "boundaryFlow"`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return read;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=request-tool-axes.js.map
|
package/dist/run-local.js
CHANGED
|
@@ -173,6 +173,7 @@ export const NO_TTY_ASK_CLASS_ROW = {
|
|
|
173
173
|
unstated: { remedy: "grant", autoBudgetReleases: false },
|
|
174
174
|
ordinary: { remedy: "grant", autoBudgetReleases: false },
|
|
175
175
|
requires_user_interaction: { remedy: "terminal_only", autoBudgetReleases: false },
|
|
176
|
+
cross_boundary: { remedy: "terminal_only", autoBudgetReleases: false },
|
|
176
177
|
};
|
|
177
178
|
const NO_TTY_REMEDY_SENTENCE = {
|
|
178
179
|
grant: () => `Nothing on the allow layer covers this call: the operator can clear this kind of question once with APPROVAL_GRANT ` +
|
package/dist/runs.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { type RunAbort } from "./run-cancel-context.js";
|
|
|
7
7
|
import type { ModelUsageTracker, PromptManifestTracker } from "./budget.js";
|
|
8
8
|
import type { ElicitationCoordinator } from "./elicitation.js";
|
|
9
9
|
import type { QuestionCoordinator } from "./question.js";
|
|
10
|
-
import { type ToolApprovalCoordinator } from "./tool-approval.js";
|
|
10
|
+
import { type ApprovalFrameFamily, type ToolApprovalCoordinator } from "./tool-approval.js";
|
|
11
11
|
import { type FleetRunPublisher } from "./fleet/fleet-bus.js";
|
|
12
12
|
import { type WorkflowCompletionInbox } from "./orchestration/workflow-completion-inbox.js";
|
|
13
13
|
import type { RunAcceptance } from "./run-acceptance.js";
|
|
@@ -329,6 +329,7 @@ approval?: {
|
|
|
329
329
|
streamApprovalOn: boolean;
|
|
330
330
|
windowMs: number | undefined;
|
|
331
331
|
windowMarginMs: number;
|
|
332
|
+
declaredFamilies: ReadonlySet<ApprovalFrameFamily>;
|
|
332
333
|
},
|
|
333
334
|
/** [ref]:park 投影腿的 checkpoint 读口(`deps.checkpointStore`)。在场时,durable park 的
|
|
334
335
|
* `suspended` 事件带 `toolCallId`(graft 见 {@link parkToolCallId});缺席=旧字节逐字不变。 */
|
package/dist/runs.js
CHANGED
|
@@ -271,12 +271,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
271
271
|
windowMarginMs: approval?.windowMarginMs ?? 0,
|
|
272
272
|
...(typeof spec.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: spec.limits.maxWalltimeMs } : {}),
|
|
273
273
|
nowMonotonicMs: performance.now(),
|
|
274
|
+
declaredFamilies: approval?.declaredFamilies ?? new Set(),
|
|
274
275
|
});
|
|
275
276
|
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
276
277
|
if (approval) {
|
|
277
278
|
const coordinator = approval.coordinator;
|
|
278
|
-
spec.onAsk = approvalLeg.
|
|
279
|
-
? coordinator.boundUnattendedAsk({ owner: elicitOwner ?? owner, taskId, ...(spec.sessionId ? { sessionId: spec.sessionId } : {}) })
|
|
279
|
+
spec.onAsk = approvalLeg.unattended
|
|
280
|
+
? coordinator.boundUnattendedAsk({ owner: elicitOwner ?? owner, taskId, ...(spec.sessionId ? { sessionId: spec.sessionId } : {}) }, approvalLeg.unattended)
|
|
280
281
|
: coordinator.boundAsk({
|
|
281
282
|
owner: elicitOwner ?? owner,
|
|
282
283
|
taskId,
|
|
@@ -504,7 +505,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
504
505
|
const withQuestion = () => question
|
|
505
506
|
? question.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: questionEmit, abortSignal: cancelCtrl.signal }, withElicit)
|
|
506
507
|
: withElicit();
|
|
507
|
-
const withApproval = () => approval && approvalLeg.active &&
|
|
508
|
+
const withApproval = () => approval && approvalLeg.active && approvalLeg.unattended === undefined
|
|
508
509
|
? approval.coordinator.runWithContext({
|
|
509
510
|
taskId,
|
|
510
511
|
owner: elicitOwner ?? owner,
|
package/dist/sealed-key.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "./observability/logger.js";
|
|
2
|
+
import { type CheckpointCtxSealing } from "./http/checkpoint-ctx.js";
|
|
2
3
|
/** Algorithm tag — MUST equal registry-core's `SEALED_BOX_ALG` (v1 sole value; a different alg on
|
|
3
4
|
* the wire is a loud per-model rejection in applyEffective, never a guess). */
|
|
4
5
|
export declare const SEALED_KEY_ALG = "libsodium-sealedbox-x25519";
|
|
@@ -61,6 +62,36 @@ export declare function deriveKeyId(publicKeyRaw: Uint8Array): string;
|
|
|
61
62
|
* zero loadable keys AND generation failed).
|
|
62
63
|
*/
|
|
63
64
|
export declare function ensureSealedKeyStore(dir: string, logger?: Logger): Promise<SealedKeyStore>;
|
|
65
|
+
declare const checkpointCtxKeyStoreBrand: unique symbol;
|
|
66
|
+
/**
|
|
67
|
+
* 停驻行封签那一域的保管店(品牌型):读口与 {@link SealedKeyStore} 同形,型不同 —— {@link createCheckpointCtxSealing} 只收它,模型钥匙那只店交进去是 tsc 错
|
|
68
|
+
* (「两域并成一对钥匙」那条路在型面上关掉,不靠注释)。唯一铸点 = {@link ensureCheckpointCtxSealing} 建店处(经 {@link mintCheckpointCtxKeyStore});
|
|
69
|
+
* 品牌断言的普查在 `test/owner-gate-single-owner.test.ts` ⑤b。
|
|
70
|
+
*/
|
|
71
|
+
export type CheckpointCtxKeyStore = SealedKeyStore & {
|
|
72
|
+
readonly [checkpointCtxKeyStoreBrand]: true;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 停驻行封签器的**生产工厂**(起服 web-admin 运行时与嵌入方同调这一只):在 `sealedKeyDir`(`<数据根>/sealed-key`)之下的 {@link CHECKPOINT_CTX_KEY_SUBDIR}
|
|
76
|
+
* 建这一域自己的保管店(同一只 {@link ensureSealedKeyStore}:目录 0700、钥匙文件 0600、空目录生成一把、读不了的文件跳过不删),再交 {@link createCheckpointCtxSealing}。
|
|
77
|
+
* 钥匙只在这里读进内存(起服一次):进程运行中补进目录的钥匙文件要重启才认。
|
|
78
|
+
* 抛 = 这一域没有封签器(目录建不了 / 改不了权限,或钥匙文件公私不配对);调用方当「没有封签器」处置。
|
|
79
|
+
*/
|
|
80
|
+
export declare function ensureCheckpointCtxSealing(sealedKeyDir: string, logger?: Logger): Promise<CheckpointCtxSealing>;
|
|
81
|
+
/**
|
|
82
|
+
* 停驻行凭据的**封签器**(S-1907;形与规则的属主 = `http/checkpoint-ctx.ts` 末段「凭据位密封」)—— 从**这一域自己的**保管店派生(生产 = {@link ensureCheckpointCtxSealing};
|
|
83
|
+
* 入参型 {@link CheckpointCtxKeyStore}:模型钥匙那只店进不来),信封是:封进密文的明文是 `JSON.stringify([CHECKPOINT_CTX_SEAL_ALG, sessionId, value])`。
|
|
84
|
+
* · **封**:对着保管店**最新**那把公钥 `crypto_box_seal`(只用公钥);封签记下那把钥匙的 id。
|
|
85
|
+
* · **开**:按封签上的 id **精确**选私钥(保管店的 `open`,不逐把试),再判信封:三元数组、首元是本信封的词、第二元是**这一行的会话 id**,交回第三元(封进去的 JSON 值,任何类型) ——
|
|
86
|
+
* 任何一条不成立 ⇒ 抛。域隔离由钥匙给(两域两对钥匙:模型钥匙的密文放进停驻行凭据位 ⇒ 这一域不持有那把钥匙;反过来停驻行的封签放进 `sealedApiKey` ⇒ 模型钥匙店不持有),
|
|
87
|
+
* 信封再给**行绑定**(别的会话那一行的封签搬过来开不开)与格式词 —— 持有本对象的那一层(HTTP 续跑腿)拿到的是「只开停驻行封签」的口;
|
|
88
|
+
* 私钥本身、保管目录、持有 id 列表照旧够不着(只多一个 `holds(id)`:开之前判「这把钥匙在不在本副本」,`key_not_held` 与 `unopenable` 分开报)。
|
|
89
|
+
* · 错误句不带明文:信封解析失败只报「不是停驻行封签」(`JSON.parse` 的报错会嵌入输入片段 —— 那里可能是一只模型钥匙),保管店的错误句只说哪把钥匙 / 哪种失败。
|
|
90
|
+
* 前置条件:`store` 来自 {@link ensureSealedKeyStore}(它已等过 libsodium 就绪);没就绪 ⇒ 当场响亮抛,不造一只封不了的对象。
|
|
91
|
+
* **自检**:交出之前对最新那把钥匙封一只探针、再开回来 —— 开不回来(保管档里公私钥不配对:长度都对,装载照收)⇒ 当场抛。封得进、开不出的封签器比没有更坏:
|
|
92
|
+
* 起服的一次性迁移 M28 会用它把还读得懂的明文行换成谁都开不开的密文。调用方(web-admin 运行时)把这一抛当「没有封签器」处置 ⇒ 耐久审批在场的部署起服拒。
|
|
93
|
+
*/
|
|
94
|
+
export declare function createCheckpointCtxSealing(store: CheckpointCtxKeyStore): CheckpointCtxSealing;
|
|
64
95
|
/**
|
|
65
96
|
* Best-effort public-key registration with the web-admin (`POST /api/config/execution-keys`).
|
|
66
97
|
*
|
|
@@ -86,4 +117,5 @@ export declare function reportExecutionPublicKey(opts: {
|
|
|
86
117
|
/** Test seam. */
|
|
87
118
|
fetchImpl?: typeof fetch;
|
|
88
119
|
}): Promise<void>;
|
|
120
|
+
export {};
|
|
89
121
|
//# sourceMappingURL=sealed-key.d.ts.map
|
package/dist/sealed-key.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
|
6
6
|
import { createCenterCredentialSource } from "./center-credential.js";
|
|
7
7
|
import { centerFetch } from "./web-admin-client/center-request.js";
|
|
8
8
|
import { WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
9
|
+
import { CHECKPOINT_CTX_SEAL_ALG } from "./http/checkpoint-ctx.js";
|
|
9
10
|
export const SEALED_KEY_ALG = "libsodium-sealedbox-x25519";
|
|
10
11
|
const SEAL_OVERHEAD_BYTES = 48;
|
|
11
12
|
const BASE64_STRICT = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
@@ -70,7 +71,7 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
70
71
|
await writeFile(path, JSON.stringify(record, null, 2) + "\n", { mode: 0o600 });
|
|
71
72
|
await chmod(path, 0o600);
|
|
72
73
|
keys.set(record.publicKeyId, { publicKey: kp.publicKey, privateKey: kp.privateKey, publicKeyB64, createdAt: record.createdAt });
|
|
73
|
-
logger?.info("sealed_key_generated", { publicKeyId: record.publicKeyId, publicKey: publicKeyB64, dir, note: "new X25519 sealed-box keypair —
|
|
74
|
+
logger?.info("sealed_key_generated", { publicKeyId: record.publicKeyId, publicKey: publicKeyB64, dir, note: "new X25519 sealed-box keypair in this directory — the private key exists only here: keep the directory across restarts and identical on every replica that has to open what it sealed, and back it up as a private key" });
|
|
74
75
|
}
|
|
75
76
|
const latestEntry = [...keys.entries()].sort((a, b) => (a[1].createdAt < b[1].createdAt ? 1 : a[1].createdAt > b[1].createdAt ? -1 : a[0] < b[0] ? -1 : 1))[0];
|
|
76
77
|
return {
|
|
@@ -88,6 +89,59 @@ export async function ensureSealedKeyStore(dir, logger) {
|
|
|
88
89
|
},
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
const CHECKPOINT_CTX_KEY_SUBDIR = "checkpoint-ctx-keys";
|
|
93
|
+
function mintCheckpointCtxKeyStore(store) {
|
|
94
|
+
return store;
|
|
95
|
+
}
|
|
96
|
+
export async function ensureCheckpointCtxSealing(sealedKeyDir, logger) {
|
|
97
|
+
return createCheckpointCtxSealing(mintCheckpointCtxKeyStore(await ensureSealedKeyStore(join(sealedKeyDir, CHECKPOINT_CTX_KEY_SUBDIR), logger)));
|
|
98
|
+
}
|
|
99
|
+
export function createCheckpointCtxSealing(store) {
|
|
100
|
+
const sodium = _sodium;
|
|
101
|
+
if (typeof sodium.crypto_box_seal !== "function")
|
|
102
|
+
throw new Error("createCheckpointCtxSealing: libsodium is not initialized — build the store with ensureSealedKeyStore() first");
|
|
103
|
+
const { publicKeyId } = store.latest;
|
|
104
|
+
const publicKey = b64ToBytes(store.latest.publicKey, "publicKey");
|
|
105
|
+
const sealing = createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey);
|
|
106
|
+
const probeSession = "sealed-key-self-test";
|
|
107
|
+
let roundTrip;
|
|
108
|
+
try {
|
|
109
|
+
roundTrip = sealing.open(sealing.seal(probeSession, probeSession), probeSession);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
roundTrip = undefined;
|
|
113
|
+
}
|
|
114
|
+
if (roundTrip !== probeSession) {
|
|
115
|
+
throw new Error(`createCheckpointCtxSealing: self-test failed — a value sealed against the newest public key (publicKeyId "${publicKeyId}") does not open with the private key held under that id; the key file's public and private halves do not belong together. Refusing to hand out a sealer whose seals nobody can open`);
|
|
116
|
+
}
|
|
117
|
+
return sealing;
|
|
118
|
+
}
|
|
119
|
+
function createUncheckedCheckpointCtxSealing(store, publicKeyId, publicKey) {
|
|
120
|
+
const sodium = _sodium;
|
|
121
|
+
return {
|
|
122
|
+
seal: (value, sessionId) => ({
|
|
123
|
+
alg: CHECKPOINT_CTX_SEAL_ALG,
|
|
124
|
+
publicKeyId,
|
|
125
|
+
sealed: Buffer.from(sodium.crypto_box_seal(sodium.from_string(JSON.stringify([CHECKPOINT_CTX_SEAL_ALG, sessionId, value])), publicKey)).toString("base64"),
|
|
126
|
+
}),
|
|
127
|
+
holds: (id) => store.has(id),
|
|
128
|
+
open: (sealed, sessionId) => {
|
|
129
|
+
const plaintext = store.open(sealed.sealed, sealed.publicKeyId);
|
|
130
|
+
let envelope;
|
|
131
|
+
try {
|
|
132
|
+
envelope = JSON.parse(plaintext);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error("sealed value is not a checkpoint-context seal");
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(envelope) || envelope.length !== 3 || envelope[0] !== CHECKPOINT_CTX_SEAL_ALG)
|
|
138
|
+
throw new Error("sealed value is not a checkpoint-context seal");
|
|
139
|
+
if (envelope[1] !== sessionId)
|
|
140
|
+
throw new Error("sealed value was sealed for a different session's resume context");
|
|
141
|
+
return envelope[2];
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
91
145
|
export async function reportExecutionPublicKey(opts) {
|
|
92
146
|
const { baseUrl, adminToken, key, notes, logger } = opts;
|
|
93
147
|
const manual = (why) => {
|
package/dist/security.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface SessionListItem {
|
|
|
42
42
|
* 而 `DELETE /v1/sessions/:id`(删整条会话)不在其中 —— 同一风险形、爆炸半径**更大**,却没门。
|
|
43
43
|
*
|
|
44
44
|
* 风险形逐字同源(守卫自己的注释):「无 service token + 未开 `ALLOW_UNAUTHED_WRITES`」这一形下
|
|
45
|
-
* `
|
|
45
|
+
* fleet 写档(`http/principal-gate.ts` 的 `fleetViewOf`,dev-open 臂)判 fleet-wide ⇒ 集群内**任意**调用方可对**任意** session 动手。
|
|
46
46
|
* policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」;DELETE 直接把会话删掉。
|
|
47
47
|
*
|
|
48
48
|
* ⇒ 枚举改**族判定**:新增一条破坏性会话路由时自动落进门内,不必记得回来加白名单。
|
|
@@ -89,7 +89,7 @@ export interface SessionCaller {
|
|
|
89
89
|
readonly fleet: boolean;
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
* 🔴 **会话行属主门上「无主行放行」臂的唯一判据**(S-1770 读面律;会话面谓词 `sessionOwnerReadable`、fork /
|
|
92
|
+
* 🔴 **会话行属主门上「无主行放行」臂的唯一判据**(S-1770 读面律;会话面谓词 `sessionOwnerReadable`、fork / 删除 / 同步族的存量互认臂 {@link legacyUnownedSessionOk}、
|
|
93
93
|
* 审批豁免面、notify 面的无主臂都经它,不各写一份):
|
|
94
94
|
* · 无主的**普通**会话行 ⇒ 成立(各臂照它自己的其余条件);
|
|
95
95
|
* · 无主的 **placed** 行(委派子代:属主 = 委派树根的属主,`delegated-session-owner.ts`)⇒ 只对 **fleet 调用方**成立({@link SessionCaller}:dev-open 部署上的任何调用方,
|
|
@@ -122,7 +122,12 @@ export declare const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
|
|
|
122
122
|
* 🪦 墓碑注(与第一臂同源):这是两个表示法之间的**过渡承接**。存量随会话生命周期消亡后,本臂
|
|
123
123
|
* 连同 {@link LEGACY_UNOWNED_AMNESTY_TENANT} 与几处调用点可**整删**,不留兼容层。
|
|
124
124
|
*
|
|
125
|
-
* ──
|
|
125
|
+
* ── 同步族(`…/sync/*`:拉取 / 推送 / 开导入 / 导入第二阶段)──────────────────────────────────
|
|
126
|
+
* 同步族四口自 7.134.0 同走本臂(表态见 7.134.0 发车帖;三条件不变):属主门是 `http/routes/session-sync.ts` 的一只布尔 `mayTouch`,与 fork / 删除同一只谓词、同一对读法
|
|
127
|
+
* (`sessionCallerOf` + `sessionOwnerCellOf`),不另立判据。这一族带破坏性的 import 腿,但单用户形下 `default` 是唯一用户,与已走本臂的删除同级,没有新增跨租风险;
|
|
128
|
+
* 多租形、非 `default` 主体、无主的 placed 行仍与不存在的会话同码同体。暂存句柄的归属不因本臂放宽(非 fleet 调用方只推自己开的暂存,fleet 调用方照旧可推任何一张)。
|
|
129
|
+
*
|
|
130
|
+
* ── 逐面判过、**刻意不放行**的两处(免得后人当遗漏再补一次)────────────────────────────────
|
|
126
131
|
* · `runOwnerOk` 三读面(head / audit 单读 / events):`owner === null` 那一支**本来就放行**
|
|
127
132
|
* (判据是 `owner !== null && principal !== owner`),不需要本臂。[ref] 帖文里「单读 404」一句
|
|
128
133
|
* 经亲读实现行**不成立**,已加现状钉(session-head-probe.test.ts)锁住这个事实。
|
|
@@ -131,9 +136,6 @@ export declare const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
|
|
|
131
136
|
* 这把键,替它读写规则只能**猜**一个 principal(猜错 = 200 说已生效,其实没有哪条 run 读它)。已经存下的规则不受影响:
|
|
132
137
|
* 它们存在当初那个属主的键下,那个 principal 的 run 照读照生效(7.119.0 前 local 后端重启即把属主忘掉,于是那些规则
|
|
133
138
|
* 在跑、却读不到也删不掉 —— 修在属主落盘,不在放宽本门)。放宽它等于制造一条 fail-open,不在本臂辖域。
|
|
134
|
-
* · `…/sync/*`(PULL/PUSH/import):PULL 虽是只读、形同 workspace,但这一族**带一条破坏性 import 腿**
|
|
135
|
-
* (`isDestructiveSessionWrite` 明列),而 [ref]/[ref] 裁的是会话**读面与生命周期**四门。放宽一个
|
|
136
|
-
* 写面族超出裁定射程 —— 要做另立一次表态,不在本臂里顺手带过。
|
|
137
139
|
*
|
|
138
140
|
* @param requirePrincipal `config.requirePrincipal`(条件①)
|
|
139
141
|
* @param caller 调用方(条件③ 读它验过的主体;fleet-wide 面不经本臂)
|
|
@@ -146,7 +148,7 @@ export declare function legacyUnownedSessionOk(requirePrincipal: boolean, caller
|
|
|
146
148
|
* 🔴 单独存在的唯一理由是**列表面**:`GET /v1/sessions` 的第三条件(`owner === null`)不在 JS 里判,
|
|
147
149
|
* 它是 store 侧 `owner = ? OR owner IS NULL` 这一句 SQL 谓词 —— 三条件仍然齐,只是第三条件由 SQL 承载
|
|
148
150
|
* (`listSessions({ owner, includeUnowned })` 的 `includeUnowned` 位即本函数的返回值)。
|
|
149
|
-
* 单实体读/写点(fork/delete
|
|
151
|
+
* 单实体读/写点(fork/delete/同步族/workspace)**一律**用三条件齐的 {@link legacyUnownedSessionOk},不要用本函数。
|
|
150
152
|
*/
|
|
151
153
|
export declare function legacyUnownedAmnestyOpen(requirePrincipal: boolean, gateOwner: string | null): boolean;
|
|
152
154
|
export type OwnerAwareSessionStore = SessionStore & {
|
|
@@ -39,8 +39,11 @@
|
|
|
39
39
|
* (请求 core 把种类放进错误,而不是在这里解析句子)。
|
|
40
40
|
* · `invalid_entry` / `invalid_fork_target` —— 留白:本仓今天没有一条 wire 口把它们作为自己的
|
|
41
41
|
* 应答(它们在 core 的追加 / 分叉面上抛,落进各站点既有的处置或通用 500)。给它们起名是另一件事、另一张票。
|
|
42
|
-
* · `
|
|
43
|
-
* (`
|
|
42
|
+
* · `conflict` —— `import` 面 **409 `conflict`**(固定句 {@link STAGED_IMPORT_OWNER_CHANGED_SENTENCE});`open` 面留白。import 面上这个词只有一个来源:
|
|
43
|
+
* 暂存提交的属主 CAS(`session-sync-kernel.ts` 的 `StagedCommitOwnerChangedError`,三家店在提交事务里比「此刻的属主格 = 这次导入的门判过的那一格」,
|
|
44
|
+
* 不等 ⇒ 回滚)—— 码位与同族的提交期同步冲突(`SyncConflictError` ⇒ 409 `conflict`「sync conflict」)同一个,出路也相同:从第一阶段重来(重新过门)。
|
|
45
|
+
* open 面上它是 run 的写撞上真并发写,落成那条 run 的 `failed` 终局(`terminal.code: "conflict"`),提交腿不另作处置。
|
|
46
|
+
* · `storage` / `unknown` —— 留白:故障类,通用 500 就是它们今天的应答。
|
|
44
47
|
*
|
|
45
48
|
* core 给闭集加一个词 ⇒ 下面的 `switch` 编译红,由人裁定它在两面上的答复,而不是让它静默落进某个恰好存在的臂
|
|
46
49
|
* ([ref] 词表纪律;同形先例 `session-policy-wire.ts` 的 `sessionPolicyErrorHttp`)。
|
|
@@ -55,9 +58,8 @@ export interface SessionErrorWire {
|
|
|
55
58
|
readonly status: number;
|
|
56
59
|
readonly errorCode: string;
|
|
57
60
|
/**
|
|
58
|
-
* 固定句(缺席 = 发 core 的原句,过既有脱敏)
|
|
59
|
-
*
|
|
60
|
-
* 常量锚格),本批逐字不改。
|
|
61
|
+
* 固定句(缺席 = 发 core 的原句,过既有脱敏)。import 面的三行用它(`invalid_session` / `placement_refused` / `conflict`):店与引擎的原句
|
|
62
|
+
* 点名条目 id / 会话 id / 引擎内部词,wire 上只回一句固定话(各自冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格)。
|
|
61
63
|
*/
|
|
62
64
|
readonly sentence?: string;
|
|
63
65
|
/** 机读事实(体顶层的 `detail` 键;缺席 = 体里不带这个键)。只有 import 面的 `placement_refused` 用它:`{ placement_kind }`。 */
|
|
@@ -70,6 +72,11 @@ export declare const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed
|
|
|
70
72
|
* (「placement partition」「delegation transcript」);固定句只说调用方能做什么(冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格)。
|
|
71
73
|
*/
|
|
72
74
|
export declare const PLACED_SESSION_SENTENCE = "this session holds a delegated agent's transcript, so session sync cannot overwrite it; sync into a different session id";
|
|
75
|
+
/**
|
|
76
|
+
* import 面 `conflict` 的固定句:暂存提交时会话的属主格已不是开暂存时门判过的那一格(另一副本在导入流式进行期间删建 / 认领 / 新建了它),提交事务回滚。
|
|
77
|
+
* 不发店的原句(它点名会话 id);固定句只说发生了什么与出路(冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格)。
|
|
78
|
+
*/
|
|
79
|
+
export declare const STAGED_IMPORT_OWNER_CHANGED_SENTENCE = "session owner changed while the import streamed; nothing was imported \u2014 start the sync again";
|
|
73
80
|
export declare function sessionErrorHttp(code: SessionErrorCode, face: SessionErrorFace): SessionErrorWire | undefined;
|
|
74
81
|
export declare function isSessionErrorCode(code: string): code is SessionErrorCode;
|
|
75
82
|
//# sourceMappingURL=session-error-wire.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed validation";
|
|
2
2
|
export const PLACED_SESSION_SENTENCE = "this session holds a delegated agent's transcript, so session sync cannot overwrite it; sync into a different session id";
|
|
3
3
|
const PLACEMENT_KIND = "subagent";
|
|
4
|
+
export const STAGED_IMPORT_OWNER_CHANGED_SENTENCE = "session owner changed while the import streamed; nothing was imported — start the sync again";
|
|
4
5
|
const CORRUPT_SESSION = { status: 500, errorCode: "corrupt_session" };
|
|
5
6
|
const INVALID_ENTRIES = { status: 422, errorCode: "invalid_entries", sentence: STAGED_IMPORT_INVALID_SENTENCE };
|
|
6
7
|
const SESSION_PLACED = { status: 409, errorCode: "conflict.session_placed", sentence: PLACED_SESSION_SENTENCE, detail: { placement_kind: PLACEMENT_KIND } };
|
|
8
|
+
const STAGED_IMPORT_OWNER_CHANGED = { status: 409, errorCode: "conflict", sentence: STAGED_IMPORT_OWNER_CHANGED_SENTENCE };
|
|
7
9
|
export function sessionErrorHttp(code, face) {
|
|
8
10
|
switch (code) {
|
|
9
11
|
case "corrupt_session":
|
|
@@ -12,11 +14,12 @@ export function sessionErrorHttp(code, face) {
|
|
|
12
14
|
return face === "import" ? INVALID_ENTRIES : undefined;
|
|
13
15
|
case "placement_refused":
|
|
14
16
|
return face === "import" ? SESSION_PLACED : undefined;
|
|
17
|
+
case "conflict":
|
|
18
|
+
return face === "import" ? STAGED_IMPORT_OWNER_CHANGED : undefined;
|
|
15
19
|
case "not_found":
|
|
16
20
|
case "invalid_entry":
|
|
17
21
|
case "invalid_fork_target":
|
|
18
22
|
case "storage":
|
|
19
|
-
case "conflict":
|
|
20
23
|
case "unknown":
|
|
21
24
|
return undefined;
|
|
22
25
|
default: {
|