@sema-agent/server 7.76.0 → 7.76.1

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.
@@ -5,7 +5,7 @@ import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugi
5
5
  import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
- import { verifyDirectDoorProof } from "../../principal-jwt.js";
8
+ import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
9
9
  import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
10
10
  import { MAX_APPROVAL_NONCE_CHARS } from "../../plugins/approval-nonce-store.js";
11
11
  import { redactedPreview, redactSecrets } from "../../trace/redact.js";
@@ -352,6 +352,10 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
352
352
  sendError(res, 400, "request.field_conflict", "decision 'edit' requires a non-empty editedPlan (the operator's revised plan)");
353
353
  return;
354
354
  }
355
+ if (decision === "edit" && !isWellFormedUnicode(body.editedPlan)) {
356
+ sendError(res, 400, "request.field_invalid", "editedPlan must be well-formed Unicode text (a lone surrogate cannot be signed unambiguously)");
357
+ return;
358
+ }
355
359
  if (decision !== "edit" && body.editedPlan !== undefined) {
356
360
  sendError(res, 400, "request.field_conflict", `editedPlan is only valid with decision 'edit' (got '${decision}')`);
357
361
  return;
@@ -399,7 +403,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
399
403
  }
400
404
  const storeNowMs = await store.nowMs();
401
405
  const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
402
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId: run.sessionId, boundCallId: planNonce ?? null, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
406
+ const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId: run.sessionId, boundCallId: planNonce ?? null, boundInputHash: decision === "edit" ? editedPlanBinding(body.editedPlan) : null, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
403
407
  if (!proof.ok) {
404
408
  sendError(res, proof.status, proof.errorCode, proof.error);
405
409
  return;
@@ -1,5 +1,9 @@
1
1
  import { BaseSessionStorage, type PromptEpochDescriptor, type SessionMetadata, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
2
2
  import type { Pool } from "mysql2/promise";
3
+ export declare const SESSION_EVENT_LATEST_BY_TYPE_SQL: {
4
+ readonly floor: "SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1";
5
+ readonly epochAnchor: "SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100";
6
+ };
3
7
  /**
4
8
  * TiDB-backed {@link BaseSessionStorage} — the durable L1 event log for one session.
5
9
  *
@@ -3,6 +3,11 @@ import { BaseSessionStorage, SessionError, leafIdAfterEntry, normalizePromptEpoc
3
3
  import { toIso } from "./sql-row-helpers.js";
4
4
  import { isMysqlDupKeyError } from "./sql-errors.js";
5
5
  import { PLACEMENT_SELECT_COLUMNS, placementFromRow } from "./session-placement.js";
6
+ const SESSION_EVENT_BY_TYPE_HINT = "/*+ USE_INDEX(session_event, idx_session_event_type) */";
7
+ export const SESSION_EVENT_LATEST_BY_TYPE_SQL = {
8
+ floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
9
+ epochAnchor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`,
10
+ };
6
11
  const isDupKey = isMysqlDupKeyError;
7
12
  export class TiDBSessionStorage extends BaseSessionStorage {
8
13
  pool;
@@ -19,7 +24,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
19
24
  return undefined;
20
25
  let cursor = this.floorSeq;
21
26
  for (;;) {
22
- const [rows] = await this.pool.query("SELECT seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100", [this.sessionId, cursor]);
27
+ const [rows] = await this.pool.query(SESSION_EVENT_LATEST_BY_TYPE_SQL.epochAnchor, [this.sessionId, cursor]);
23
28
  if (rows.length === 0)
24
29
  return undefined;
25
30
  for (const r of rows) {
@@ -44,7 +49,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
44
49
  return new TiDBSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
45
50
  }
46
51
  static async computeFloor(pool, sessionId) {
47
- const [compRows] = await pool.query("SELECT payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1", [sessionId]);
52
+ const [compRows] = await pool.query(SESSION_EVENT_LATEST_BY_TYPE_SQL.floor, [sessionId]);
48
53
  if (!compRows[0])
49
54
  return { floorEntryId: null, floorSeq: 0 };
50
55
  const comp = (typeof compRows[0].payload === "string" ? JSON.parse(compRows[0].payload) : compRows[0].payload);
@@ -30,6 +30,23 @@ export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[
30
30
  /** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
31
31
  * CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
32
32
  * Binds the identity assertion to THIS specific suspended action so a captured JWT cannot be lifted onto another. */
33
+ /**
34
+ * 直连门信封**第三槽**的 plan_review 半场:**决定所作用内容的哈希**。
35
+ *
36
+ * 第三槽在两种门上是**同一条规则**「这份决定作用在哪份内容上」:动作门填被批工具调用的 `boundInputHash`
37
+ * (core 铸);非动作门 `plan_review` 的 `edit` 填**修订后的计划**的哈希(计划随本次请求一起提交,签名器手上
38
+ * 就有原文),`approve` / `reject` 不作用于任何新内容 ⇒ null。B-121(test 黑盒发现,验真后修):此前 edit 也填
39
+ * null,于是为「只改 README」签出的证明可以拿去提交任意 `editedPlan`——签名只护住了 decision/reason,没护住
40
+ * 决定真正落地的那份内容。`cnf.bnd` 与 HMAC 都吃第三槽,所以 JWT 与信封同时绑住它;不匹配 ⇒
41
+ * `401 principal_unbound`(既有码,cnf.bnd 不符)。旧注记「计划哈希被拒」说的是 approve(worker 没有可哈希的
42
+ * 原文),不覆盖 edit。
43
+ *
44
+ * 形:`sha256(utf8(editedPlan))` hex —— 与 `approvalBnd` 同摘要、同编码;逐字节,不归一化(签名器与 worker
45
+ * 必须对同一串字节达成一致,任何"归一化"都是第二个写者)。
46
+ */
47
+ /** 良构 Unicode(无孤立代理项)。`u` 旗下配对代理项是一个码点、不匹配 `\p{Surrogate}`;只有孤立的才命中。 */
48
+ export declare function isWellFormedUnicode(s: string): boolean;
49
+ export declare function editedPlanBinding(editedPlan: string): string;
33
50
  export declare function approvalBnd(env: {
34
51
  sessionId: string;
35
52
  boundCallId?: string | null;
@@ -81,9 +98,12 @@ export type DirectDoorProofResult = {
81
98
  * SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
82
99
  * (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
83
100
  * action), so the proof correctly DEGRADES to "verify WHO": JWT identity + cnf.bnd channel-binding + the HMAC over
84
- * {sessionId, decision} (bound fields null) — WITHOUT requiring a boundInputHash. Pass `actionBinding:false` for a
85
- * non-action gate; the default (true) keeps the /decide tool-approval contract byte-identical. Binding the decision
86
- * to a plan HASH was REJECTED (the plan artifact lives in a profile ref store; the worker has no TOCTOU original).
101
+ * {sessionId, nonce, <content hash>, decision, reason}. Pass `actionBinding:false` for a non-action gate; the default
102
+ * (true) keeps the /decide tool-approval contract byte-identical. The THIRD slot carries one rule on both gates —
103
+ * "the content this decision acts on": the bound tool input for /decide, `editedPlanBinding(editedPlan)` for a
104
+ * plan_review `edit` (B-121), null for approve/reject. Binding approve/reject to a hash of the EXISTING plan was
105
+ * rejected (the plan artifact lives in a profile ref store; the worker has no TOCTOU original) — that ruling never
106
+ * covered `edit`, whose content arrives with the request.
87
107
  */
88
108
  export declare function verifyDirectDoorProof(proof: {
89
109
  jwt?: string;
@@ -72,6 +72,14 @@ export function verifyPrincipalJwt(token, keys, expect) {
72
72
  const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
73
73
  return { sub: payload.sub, bnd, acceptedBeforeMs: (Math.floor(payload.exp + skew) + 1) * 1000 };
74
74
  }
75
+ export function isWellFormedUnicode(s) {
76
+ return !/\p{Surrogate}/u.test(s);
77
+ }
78
+ export function editedPlanBinding(editedPlan) {
79
+ if (!isWellFormedUnicode(editedPlan))
80
+ throw new Error("editedPlanBinding: editedPlan is not well-formed Unicode (lone surrogate) — utf8 hashing would be lossy");
81
+ return createHash("sha256").update(editedPlan, "utf8").digest("hex");
82
+ }
75
83
  export function approvalBnd(env) {
76
84
  return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
77
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.76.0",
3
+ "version": "7.76.1",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.17.6",
57
+ "@sema-agent/core": "7.17.7",
58
58
  "@sema-agent/settings-schema": "1.10.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",