@sema-agent/server 7.76.1 → 7.77.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.
Files changed (61) hide show
  1. package/USAGE.md +47 -21
  2. package/dist/boot/engine-lease.d.ts +89 -11
  3. package/dist/boot/engine-lease.js +26 -11
  4. package/dist/boot/exit-last.d.ts +24 -0
  5. package/dist/boot/exit-last.js +46 -0
  6. package/dist/boot/shutdown.js +28 -4
  7. package/dist/config-catalog.js +3 -2
  8. package/dist/config-types.d.ts +23 -11
  9. package/dist/config.js +8 -8
  10. package/dist/fleet/subagent-tail-bus.d.ts +28 -2
  11. package/dist/fleet/subagent-tail-bus.js +24 -2
  12. package/dist/http/principal-gate.d.ts +43 -1
  13. package/dist/http/principal-gate.js +23 -1
  14. package/dist/http/routes/a2a-serve.js +2 -2
  15. package/dist/http/routes/admin-config-refresh.js +2 -2
  16. package/dist/http/routes/admin-drain.js +2 -2
  17. package/dist/http/routes/admin-memory-optout.js +2 -2
  18. package/dist/http/routes/adoption.js +2 -2
  19. package/dist/http/routes/approvals-assistant.js +4 -4
  20. package/dist/http/routes/attachments.js +2 -2
  21. package/dist/http/routes/config-catalog.js +2 -2
  22. package/dist/http/routes/devices.js +2 -2
  23. package/dist/http/routes/fleet.js +2 -2
  24. package/dist/http/routes/leader.js +2 -2
  25. package/dist/http/routes/memory-bundle.js +2 -2
  26. package/dist/http/routes/memory-compliance.js +2 -2
  27. package/dist/http/routes/memory-consolidation.js +2 -2
  28. package/dist/http/routes/memory-origin.js +2 -2
  29. package/dist/http/routes/memory-policy.js +2 -2
  30. package/dist/http/routes/notify-wake.js +3 -3
  31. package/dist/http/routes/observability.js +2 -2
  32. package/dist/http/routes/retention-ops.js +2 -2
  33. package/dist/http/routes/run-memory-capture.js +2 -2
  34. package/dist/http/routes/runs.js +13 -13
  35. package/dist/http/routes/sessions.js +53 -12
  36. package/dist/http/routes/side-query.js +2 -2
  37. package/dist/http/routes/tasks.js +14 -10
  38. package/dist/http/routes/workflows.js +3 -3
  39. package/dist/http/server.js +10 -23
  40. package/dist/observability/fail-open.d.ts +4 -0
  41. package/dist/observability/fail-open.js +4 -0
  42. package/dist/parent-watch.d.ts +32 -18
  43. package/dist/parent-watch.js +12 -10
  44. package/dist/parked-decide.js +1 -1
  45. package/dist/plugins/file-run-store.d.ts +6 -1
  46. package/dist/plugins/file-run-store.js +10 -0
  47. package/dist/plugins/memory-run-store.d.ts +6 -1
  48. package/dist/plugins/memory-run-store.js +10 -0
  49. package/dist/plugins/run-store-sql.d.ts +13 -2
  50. package/dist/plugins/run-store-sql.js +5 -0
  51. package/dist/plugins/store-contracts.d.ts +16 -0
  52. package/dist/runs.js +4 -10
  53. package/dist/tool-approval.js +2 -2
  54. package/dist/trace/ledger-sink.js +3 -14
  55. package/dist/trace/model-segment.d.ts +55 -0
  56. package/dist/trace/model-segment.js +58 -0
  57. package/dist/trace/project.d.ts +17 -0
  58. package/dist/trace/project.js +9 -1
  59. package/dist/trace/redact.d.ts +23 -1
  60. package/dist/trace/redact.js +49 -15
  61. package/package.json +1 -1
@@ -7,7 +7,9 @@ export type SubagentTailFrame = Record<string, unknown> & {
7
7
  type: string;
8
8
  };
9
9
  /** core forward 事件 → wire 投影(sync 主流五分支同源;不认识的帧类型=null 不发——tail 面只承诺
10
- * content 六类,新 core 帧类型先经这里显式收编再出 wire)。 */
10
+ * content 六类,新 core 帧类型先经这里显式收编再出 wire)。
11
+ * ⚠️ **本函数是无状态逐帧投影**:`reasoning_end`(S-310 的推理权威段)不在这里 —— 它要按句柄攒段,
12
+ * 住在 {@link SubagentTailBus.onForwardFrame}(publish 侧是唯一知道句柄的地方)。 */
11
13
  export declare function projectTailFrame(e: Record<string, unknown> & {
12
14
  type?: string;
13
15
  }): SubagentTailFrame | null;
@@ -50,9 +52,33 @@ export declare class SubagentTailBus {
50
52
  * 不需要"先有内容帧"才能学到"没有内容帧"。
51
53
  */
52
54
  private readonly modeByHandle;
55
+ /**
56
+ * 句柄 → 该句柄这条 tail 流上**在攒的推理段**(S-310)。
57
+ *
58
+ * 键必须是句柄,理由与 {@link modeByHandle} 同源再加一条硬的:两个并发子代的 `reasoning_delta` 在
59
+ * **同一个** forward sink 上交错到达,一只累积器会把两份思考拼成一段、再盖上其中一个的身份键。
60
+ *
61
+ * 生命期 = **有人在听的那段时间**:攒段住在 `hasSubscribers` 快门之内(tail 是 live-only 面,没人听
62
+ * 就不投影也不攒),最后一个订阅者走掉或句柄终态({@link forgetHandle})时整条删掉 ⇒ 本表恒被
63
+ * 订阅者数量所界,不需要第二道容量闸。
64
+ */
65
+ private readonly segByHandle;
53
66
  /** 三条 run 腿的 forward 回调里逐帧调(在 `hasSubscribers` 快门**之前**——没人在听时也要学,
54
67
  * 否则晚开的 tail 永远读不到供给形)。同值重复写是 no-op(热路径只付一次 Map.get)。 */
55
68
  noteHandleContentMode(bgAgentId: string, mode: TailContentMode): void;
69
+ /**
70
+ * 🔴 **三条 run 腿的 forward 回调唯一入口**(S-310 收口)——登记供给形 → 段界收段 → 逐帧投影 → publish。
71
+ *
72
+ * 此前这四步以**同一段六行代码**抄在三条腿上(sync 流腿、verify-resume 腿、resume-stream 腿),
73
+ * 差别为零;S-310 要给 tail 面补 `reasoning_end` 时,那就是第四、第五、第六份拷贝。收成一扇门之后
74
+ * 腿上只剩「句柄在不在」这一句判断,三腿不可能再分家(`test/subagent-tail-stream.test.ts` G18-G20)。
75
+ *
76
+ * 顺序即语义:权威段**先于**触发它的结构帧上 wire(消费端「整段替换已攒 delta」的替换点不与卡片交错),
77
+ * 与账本腿「文本行先于结构行」逐字同构。
78
+ */
79
+ onForwardFrame(bgAgentId: string, mode: TailContentMode, e: Record<string, unknown> & {
80
+ type?: string;
81
+ }): void;
56
82
  /**
57
83
  * 句柄终态时**撤销**登记(三条腿的 `onTaskNotification` 观察者调,与 `task_settled` 帧同处)。
58
84
  *
@@ -69,7 +95,7 @@ export declare class SubagentTailBus {
69
95
  * 的答案。core 今天把终态通知排在最后一条 forward 之后(fleet-bus 同注),复活又是人手动作,这条窗
70
96
  * 实际上不可达;真要关死需要给句柄加墓碑态,代价大于一位诊断标签的收益,故不做。
71
97
  */
72
- forgetHandleContentMode(bgAgentId: string): void;
98
+ forgetHandle(bgAgentId: string): void;
73
99
  /** 路由读(按 tail 的 target 句柄)。缺席 = 不知道,路由出 `"unknown"`。 */
74
100
  handleContentMode(bgAgentId: string): TailContentMode | undefined;
75
101
  /** 两条 run 腿的 fwdInternals 调用点:帧已投影(projectTailFrame),null 不进来。 */
@@ -1,5 +1,6 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
2
  import { taskProgressEventData, toolStartEventData, toolEndEventData, textEndEventData } from "../trace/project.js";
3
+ import { createReasoningSegments } from "../trace/model-segment.js";
3
4
  const TAIL_QUEUE_CAP = 512;
4
5
  export function projectTailFrame(e) {
5
6
  const t = e.type;
@@ -41,12 +42,31 @@ const HOST_MODE_CAP = 4096;
41
42
  export class SubagentTailBus {
42
43
  subs = new Map();
43
44
  modeByHandle = new Map();
45
+ segByHandle = new Map();
44
46
  noteHandleContentMode(bgAgentId, mode) {
45
47
  if (this.modeByHandle.get(bgAgentId) === mode)
46
48
  return;
47
49
  putBounded(this.modeByHandle, bgAgentId, mode);
48
50
  }
49
- forgetHandleContentMode(bgAgentId) {
51
+ onForwardFrame(bgAgentId, mode, e) {
52
+ this.noteHandleContentMode(bgAgentId, mode);
53
+ if (!this.hasSubscribers(bgAgentId))
54
+ return;
55
+ let segs = this.segByHandle.get(bgAgentId);
56
+ if (segs === undefined) {
57
+ segs = createReasoningSegments();
58
+ this.segByHandle.set(bgAgentId, segs);
59
+ }
60
+ for (const end of segs.onEvent(e))
61
+ this.publish(bgAgentId, end);
62
+ const f = projectTailFrame(e);
63
+ if (f)
64
+ this.publish(bgAgentId, f);
65
+ }
66
+ forgetHandle(bgAgentId) {
67
+ for (const end of this.segByHandle.get(bgAgentId)?.cut() ?? [])
68
+ this.publish(bgAgentId, end);
69
+ this.segByHandle.delete(bgAgentId);
50
70
  this.modeByHandle.delete(bgAgentId);
51
71
  }
52
72
  handleContentMode(bgAgentId) {
@@ -84,8 +104,10 @@ export class SubagentTailBus {
84
104
  sub.closed = true;
85
105
  sub.wake?.();
86
106
  set.delete(sub);
87
- if (set.size === 0 && this.subs.get(bgAgentId) === set)
107
+ if (set.size === 0 && this.subs.get(bgAgentId) === set) {
88
108
  this.subs.delete(bgAgentId);
109
+ this.segByHandle.delete(bgAgentId);
110
+ }
89
111
  };
90
112
  const it = {
91
113
  [Symbol.asyncIterator]() {
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage } from "node:http";
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { ServiceConfig } from "../config-types.js";
3
3
  /** Coerce a possibly-repeated request header to a single string (or undefined). */
4
4
  export declare function headerStr(v: string | string[] | undefined): string | undefined;
@@ -21,6 +21,48 @@ export declare function isLoopbackAddress(addr: string | undefined): boolean;
21
21
  */
22
22
  export declare function verifiedIdentitySource(req: IncomingMessage, config: ServiceConfig): "sso" | "direct_door_jwt" | "service_token" | undefined;
23
23
  export declare function gatedPrincipal(req: IncomingMessage, config: ServiceConfig): string | undefined;
24
+ /**
25
+ * 「**出示了凭证但验不过** ≠ **没出示**」在 **direct-door principal JWT** 面上的形 —— 本仓
26
+ * {@link bearerPresentedButUnverified} 早就是同一条律在 Bearer 面的形,这里只是把它推到另一面。
27
+ *
28
+ * 判据三段,缺一不成立:①这台机器是直连门(BFF/gated 上 `principalHeader` 由上游验过,本函数恒 false ⇒
29
+ * 那条部署形上零行为变化);②`X-Approval-Principal-Token` **在场**(缺席 = 真的没出示,那是
30
+ * `auth.principal_required` 的语义,不许借本码);③它**验不过**({@link verifiedPrincipal} 回 undefined)。
31
+ */
32
+ export declare function principalTokenPresentedButUnverified(req: IncomingMessage, config: ServiceConfig): boolean;
33
+ /**
34
+ * 🔴 **`requirePrincipal` 部署上「这次请求没有可用身份」的唯一 401 铸点**(S-305,7.76.2)。
35
+ *
36
+ * 此前 45 个路由各自手抄同一行 `sendError(res, 401, "auth.principal_required", …)`,于是直连门上
37
+ * 「JWT 出示了但过期/签不上」与「什么头都没带」在 wire 上**同形**(黑盒实测:plan_review 带过期 JWT
38
+ * 拿到 `auth.principal_required`,而契约与路由层的 `verifyDirectDoorProof` 说的是 `principal_unverified`)。
39
+ * 两件事的处置完全不同 —— 前者「重签一张再来」,后者「你压根没接上身份面」。
40
+ *
41
+ * 一条规则、一处落点:{@link principalTokenPresentedButUnverified} 为真 ⇒ `401 principal_unverified`
42
+ * (既有码,文案与 `principal-jwt.ts` 的验签层同句);否则 `401 auth.principal_required`(原样)。
43
+ * 路由层 `verifyDirectDoorProof` 自己那条 `principal_unverified` 臂保留 —— 它守的是 decide/plan_review
44
+ * 调用链上的**证明三件套**,与本门(身份面)是两道门。
45
+ */
46
+ export declare function sendPrincipalRefusal(res: ServerResponse, req: IncomingMessage, config: ServiceConfig): void;
47
+ /**
48
+ * 🔴 **「这个调用方读得到这一行 run 吗」的唯一谓词**(S-297 抽取;此前只活在 `server.ts runOwnerOk` 的
49
+ * 函数体里,而 `runOwnerOk` 把**判断**与**404 怎么发**焊在一起 —— 于是任何需要同一条判断但不想 404 的
50
+ * 读面(如 `/mcp` 面板的 `lastLegMcp`:非属主就不投这个键、面板照常 200)只能手抄一份,而手抄那份必然
51
+ * 漏掉下面这条窄互认臂:`explicitOperatorOk` 顶注已经为同族的病写过「SINGLE mint point,不要再造兄弟」)。
52
+ *
53
+ * 三条放行臂,别的一律不放:
54
+ * · `owner === null` —— 无属主的行(dev / 存量),本来就是开放的;
55
+ * · `principal === owner` —— 自己的行。⚠️ `principal` 必须是**验过**的身份(`gatedPrincipal`),
56
+ * 不是自述头 —— `owner` 是创建时从验过的 principal 落库的,拿自述头来比就等于谁都能读别人的 run;
57
+ * · [ref] C8 **窄互认**:非 `requirePrincipal` 部署上那批 `anon:shell-live` 哨兵行 + 无身份的调用方
58
+ * (cli 停发哨兵头后的存量会话,否则「能续跑但看不见」)。`gatedPrincipal` 的回值是 `string | undefined`
59
+ * ⇒ 原文那半个 `principal === null` 比较结构上不可达,抽取时一并去掉(语义逐字不变)。
60
+ *
61
+ * **不含 operator 臂**(刻意,[ref]⑥ 台账):跨属主 operator 在 poll/events/cancel 三读面上今天就是 404,
62
+ * 放宽它是一次独立裁定。需要 operator 臂的读面在**自己的调用点**显式并上 {@link explicitOperatorOk} ——
63
+ * 那样「谁多给了 operator 一条路」在代码上看得见,而不是被偷偷塞进共享谓词里。
64
+ */
65
+ export declare function runOwnerReadable(principal: string | undefined, owner: string | null, config: ServiceConfig): boolean;
24
66
  /** The EXPLICIT operator gate (IMAGE-API-DESIGN.md §P2.4a MUST-FIX) — NOT the bare `isOperator` (whose empty-list
25
67
  * "true-for-all" is an unrestricted-write exec door the instant OPERATOR_PRINCIPALS is empty). Requires a non-empty
26
68
  * operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
@@ -1,5 +1,6 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
- import { verifiedPrincipal, ssoVerifiedPrincipal } from "../security.js";
2
+ import { PRINCIPAL_TOKEN_HEADER, verifiedPrincipal, ssoVerifiedPrincipal } from "../security.js";
3
+ import { sendError } from "./send.js";
3
4
  export function headerStr(v) {
4
5
  return Array.isArray(v) ? v[0] : v;
5
6
  }
@@ -50,6 +51,27 @@ export function gatedPrincipal(req, config) {
50
51
  return undefined;
51
52
  return verifiedPrincipal(req, config);
52
53
  }
54
+ export function principalTokenPresentedButUnverified(req, config) {
55
+ if (!config.directDoorActive)
56
+ return false;
57
+ if (headerStr(req.headers[PRINCIPAL_TOKEN_HEADER]) === undefined)
58
+ return false;
59
+ return verifiedPrincipal(req, config) === undefined;
60
+ }
61
+ export function sendPrincipalRefusal(res, req, config) {
62
+ if (principalTokenPresentedButUnverified(req, config)) {
63
+ sendError(res, 401, "principal_unverified", "principal token failed verification");
64
+ return;
65
+ }
66
+ sendError(res, 401, "auth.principal_required", `missing principal header '${config.principalHeader}'`);
67
+ }
68
+ export function runOwnerReadable(principal, owner, config) {
69
+ if (owner === null)
70
+ return true;
71
+ if (principal === owner)
72
+ return true;
73
+ return !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
74
+ }
53
75
  export function explicitOperatorOk(principal, operatorPrincipals) {
54
76
  return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
55
77
  }
@@ -6,7 +6,7 @@ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-cont
6
6
  import { HttpError } from "../../security.js";
7
7
  import { scopedIdempotencyKey } from "../idempotency.js";
8
8
  import { sendJson, sendError } from "../send.js";
9
- import { gatedPrincipal } from "../principal-gate.js";
9
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
10
10
  import { createDurableRun } from "./runs.js";
11
11
  export const A2A_PROTOCOL_VERSION = "1.0";
12
12
  export const A2A_VERSION_HEADER = "a2a-version";
@@ -344,7 +344,7 @@ async function handleA2aRpcBody(req, res, url, ctx, miss) {
344
344
  res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
345
345
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
346
346
  deps.metrics?.inc("a2a_serve_rejected_total", { reason: "principal_required" });
347
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
347
+ sendPrincipalRefusal(res, req, deps.config);
348
348
  return;
349
349
  }
350
350
  let payload;
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
2
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  export const ADMIN_CONFIG_REFRESH_PATH = "/v1/admin/config/refresh";
4
4
  const OPERATOR_ONLY_MESSAGE = "config refresh is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable this endpoint)";
5
5
  export async function handleAdminConfigRefresh(req, res, url, ctx) {
@@ -16,7 +16,7 @@ async function handleAdminConfigRefreshBody(req, res, url, ctx, miss) {
16
16
  }
17
17
  const principal = gatedPrincipal(req, deps.config);
18
18
  if (deps.config.requirePrincipal && !principal) {
19
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
19
+ sendPrincipalRefusal(res, req, deps.config);
20
20
  return;
21
21
  }
22
22
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
2
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
4
4
  export const MAX_DRAIN_REASON_CHARS = 256;
5
5
  const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable this endpoint)";
@@ -17,7 +17,7 @@ async function handleAdminDrainBody(req, res, url, ctx, miss) {
17
17
  }
18
18
  const principal = gatedPrincipal(req, deps.config);
19
19
  if (deps.config.requirePrincipal && !principal) {
20
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
20
+ sendPrincipalRefusal(res, req, deps.config);
21
21
  return;
22
22
  }
23
23
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
5
5
  export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
6
6
  export const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE = /^\/v1\/admin\/memory-optout\/([^/]+)$/;
@@ -38,7 +38,7 @@ async function handleBody(req, res, url, ctx, miss) {
38
38
  }
39
39
  const principal = gatedPrincipal(req, deps.config);
40
40
  if (deps.config.requirePrincipal && !principal) {
41
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
41
+ sendPrincipalRefusal(res, req, deps.config);
42
42
  return;
43
43
  }
44
44
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -2,7 +2,7 @@ import { createAdoptionRunner } from "../../adoption/runner.js";
2
2
  import { AdoptionRequestSchema } from "../../adoption/wire.js";
3
3
  import { checkAdoptionPrincipalShape, checkAdoptionWidths } from "../../adoption/plan.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  const ADOPTION_ID_RE = /^\/v1\/adoption\/([^/]+)$/;
7
7
  function sendOutcome(res, out) {
8
8
  switch (out.kind) {
@@ -54,7 +54,7 @@ async function handleAdoptionBody(req, res, url, ctx, miss) {
54
54
  }
55
55
  const principal = gatedPrincipal(req, deps.config);
56
56
  if (deps.config.requirePrincipal && !principal) {
57
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
57
+ sendPrincipalRefusal(res, req, deps.config);
58
58
  return;
59
59
  }
60
60
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -14,7 +14,7 @@ import { fleetRunLabels } from "../../fleet/fleet-bus.js";
14
14
  import { sleep } from "../sse-log.js";
15
15
  import { bindSseLifecycle } from "../sse-lifecycle.js";
16
16
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
17
- import { gatedPrincipal, explicitOperatorOk, isOperator } from "../principal-gate.js";
17
+ import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
18
18
  import { governanceOriginOf } from "../active-run-conflict.js";
19
19
  import { sendResumeOutcome } from "../route-ctx.js";
20
20
  export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
@@ -246,7 +246,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
246
246
  return;
247
247
  }
248
248
  if (deps.config.requirePrincipal && principal === undefined) {
249
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
249
+ sendPrincipalRefusal(res, req, deps.config);
250
250
  return;
251
251
  }
252
252
  if (!deps.config.resourceSuspend || !deps.checkpointStore) {
@@ -310,7 +310,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
310
310
  return;
311
311
  }
312
312
  if (deps.config.requirePrincipal && principal === undefined) {
313
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
313
+ sendPrincipalRefusal(res, req, deps.config);
314
314
  return;
315
315
  }
316
316
  const taskId = resumeMatch[1];
@@ -338,7 +338,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
338
338
  return;
339
339
  }
340
340
  if (deps.config.requirePrincipal && principal === undefined) {
341
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
341
+ sendPrincipalRefusal(res, req, deps.config);
342
342
  return;
343
343
  }
344
344
  const taskId = planReviewMatch[1];
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { uuidv7, sanitizePathComponent } from "@sema-agent/core";
3
3
  import { HttpError } from "../../security.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
- import { gatedPrincipal } from "../principal-gate.js";
5
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  export async function handleAttachments(req, res, url, ctx) {
7
7
  const miss = { fell: false };
8
8
  await handleAttachmentsBody(req, res, url, ctx, miss);
@@ -23,7 +23,7 @@ async function handleAttachmentsBody(req, res, url, ctx, miss) {
23
23
  }
24
24
  const principal = gatedPrincipal(req, deps.config);
25
25
  if (deps.config.requirePrincipal && principal === undefined) {
26
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
26
+ sendPrincipalRefusal(res, req, deps.config);
27
27
  return;
28
28
  }
29
29
  const owner = principal ?? "default";
@@ -1,6 +1,6 @@
1
1
  import { DOMAINS } from "@sema-agent/settings-schema";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { configKnobTable } from "../../config.js";
5
5
  import { buildConfigCatalog } from "../../config-catalog.js";
6
6
  export const CONFIG_CATALOG_PATH = "/v1/config/catalog";
@@ -23,7 +23,7 @@ function handleConfigCatalogBody(req, res, url, ctx, miss) {
23
23
  }
24
24
  const principal = gatedPrincipal(req, deps.config);
25
25
  if (deps.config.requirePrincipal && !principal) {
26
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
26
+ sendPrincipalRefusal(res, req, deps.config);
27
27
  return;
28
28
  }
29
29
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -1,5 +1,5 @@
1
1
  import { sendError, sendJson } from "../send.js";
2
- import { explicitOperatorOk, gatedPrincipal } from "../principal-gate.js";
2
+ import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { ssoVerifiedScope } from "../../security.js";
4
4
  import { assertDeviceIdShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
5
5
  const DEVICE_NOT_FOUND_MESSAGE = "device not found";
@@ -60,7 +60,7 @@ async function handleDevicesBody(req, res, url, ctx, miss) {
60
60
  }
61
61
  const principal = gatedPrincipal(req, deps.config);
62
62
  if (deps.config.requirePrincipal && principal === undefined) {
63
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
63
+ sendPrincipalRefusal(res, req, deps.config);
64
64
  return;
65
65
  }
66
66
  const store = deps.deviceStore;
@@ -2,7 +2,7 @@ import { hookNoticeWire } from "../../fleet/fleet-bus.js";
2
2
  import { FLEET_SNAPSHOT_TERMINAL_MAX_ROWS, isTerminalWorkflowRowStatus, recentTerminalWorkflowRows } from "../../fleet/fleet-terminal-window.js";
3
3
  import { taskNotificationFoldKey } from "../../orchestration/workflow-completion-inbox.js";
4
4
  import { sendJson, sendError, sseHeaders } from "../send.js";
5
- import { gatedPrincipal } from "../principal-gate.js";
5
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  export async function handleFleet(req, res, url, ctx) {
7
7
  const miss = { fell: false };
8
8
  await handleFleetBody(req, res, url, ctx, miss);
@@ -20,7 +20,7 @@ async function handleFleetBody(req, res, url, ctx, miss) {
20
20
  const { fleetWide } = sessionOwnerScope(req);
21
21
  const principal = gatedPrincipal(req, deps.config);
22
22
  if (deps.config.requirePrincipal && !principal && !fleetWide) {
23
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
23
+ sendPrincipalRefusal(res, req, deps.config);
24
24
  return;
25
25
  }
26
26
  const fleetParams = new URL(req.url ?? "", "http://x").searchParams;
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, headerStr } from "../principal-gate.js";
2
+ import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  export async function handleLeader(req, res, url, ctx) {
4
4
  const miss = { fell: false };
5
5
  await handleLeaderBody(req, res, url, ctx, miss);
@@ -13,7 +13,7 @@ async function handleLeaderBody(req, res, url, ctx, miss) {
13
13
  const isLeaderGet = req.method === "GET" && /^\/v1\/leader\/[^/]+$/.test(url);
14
14
  if (isLeaderPost || isLeaderGet) {
15
15
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
16
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
16
+ sendPrincipalRefusal(res, req, deps.config);
17
17
  return;
18
18
  }
19
19
  const requester = gatedPrincipal(req, deps.config) ?? null;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { buildOriginFencedBundle, readOriginAware } from "../../memory-origin-fence.js";
4
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
4
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
5
  export const MEMORY_BUNDLE_EXPORT_PATH = "/v1/memory/export";
6
6
  export const MEMORY_BUNDLE_IMPORT_PATH = "/v1/memory/import";
7
7
  const ExportRequestSchema = z.object({ scopes: z.array(z.string().min(1)).min(1) });
@@ -72,7 +72,7 @@ async function handleMemoryBundleBody(req, res, url, ctx, miss) {
72
72
  }
73
73
  const principal = gatedPrincipal(req, deps.config);
74
74
  if (deps.config.requirePrincipal && !principal) {
75
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
75
+ sendPrincipalRefusal(res, req, deps.config);
76
76
  return;
77
77
  }
78
78
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  export const MEMORY_ERASE_PATH = "/v1/memory/erase";
7
7
  export const MEMORY_PROVENANCE_RE = /^\/v1\/memory\/entries\/([^/]+)\/provenance$/;
8
8
  const EraseRequestSchema = z.object({
@@ -61,7 +61,7 @@ async function handleMemoryComplianceBody(req, res, url, ctx, miss) {
61
61
  }
62
62
  const principal = gatedPrincipal(req, deps.config);
63
63
  if (deps.config.requirePrincipal && !principal) {
64
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
64
+ sendPrincipalRefusal(res, req, deps.config);
65
65
  return;
66
66
  }
67
67
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
4
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
5
  export const MEMORY_CONSOLIDATION_STATUS_PATH = "/v1/admin/memory/consolidation";
6
6
  export const MEMORY_CONSOLIDATION_RUN_PATH = "/v1/admin/memory/consolidation/run";
7
7
  const RunRequestSchema = z.object({ scope: z.string().optional() }).strict();
@@ -86,7 +86,7 @@ async function handleMemoryConsolidationBody(req, res, url, ctx, miss) {
86
86
  }
87
87
  const principal = gatedPrincipal(req, deps.config);
88
88
  if (deps.config.requirePrincipal && !principal) {
89
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
89
+ sendPrincipalRefusal(res, req, deps.config);
90
90
  return;
91
91
  }
92
92
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
7
7
  export const MEMORY_ORIGIN_CLEARANCES_PATH = "/v1/memory/origin/clearances";
8
8
  export const MEMORY_ORIGIN_CLEAR_RE = /^\/v1\/memory\/origin\/entries\/([^/]+)\/clear$/;
@@ -123,7 +123,7 @@ async function handleMemoryOriginBody(req, res, url, ctx, miss) {
123
123
  }
124
124
  const principal = gatedPrincipal(req, deps.config);
125
125
  if (deps.config.requirePrincipal && !principal) {
126
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
126
+ sendPrincipalRefusal(res, req, deps.config);
127
127
  return;
128
128
  }
129
129
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -2,7 +2,7 @@ import { formatUserScope } from "@sema-agent/core";
2
2
  import { parseMemorySyncRequest } from "../../memory-sync.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
6
  const ORG_PREFIX = "org:";
7
7
  export const MEMORY_EXPORT_MAX_LIMIT = 500;
8
8
  function encodeExportCursor(c) {
@@ -171,7 +171,7 @@ async function handleMemoryPolicyBody(req, res, url, ctx, miss) {
171
171
  if (url === "/v1/policy" && req.method === "GET") {
172
172
  const principal = gatedPrincipal(req, deps.config);
173
173
  if (deps.config.requirePrincipal && !principal) {
174
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
174
+ sendPrincipalRefusal(res, req, deps.config);
175
175
  return;
176
176
  }
177
177
  sendJson(res, 200, {
@@ -1,6 +1,6 @@
1
1
  import { taskNotificationInboxEntry } from "../../orchestration/workflow-completion-inbox.js";
2
2
  import { sendJson, sendError } from "../send.js";
3
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { sendResumeOutcome } from "../route-ctx.js";
5
5
  export const SESSION_WAKE_RE = /^\/v1\/sessions\/[^/]+\/wake$/;
6
6
  export async function handleNotifyWake(req, res, url, ctx) {
@@ -18,7 +18,7 @@ async function handleNotifyWakeBody(req, res, url, ctx, miss) {
18
18
  return;
19
19
  const principal = gatedPrincipal(req, deps.config);
20
20
  if (deps.config.requirePrincipal && principal === undefined) {
21
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
21
+ sendPrincipalRefusal(res, req, deps.config);
22
22
  return;
23
23
  }
24
24
  const notifySession = decodeURIComponent(url.split("/")[3]);
@@ -112,7 +112,7 @@ async function handleNotifyWakeBody(req, res, url, ctx, miss) {
112
112
  return;
113
113
  const principal = gatedPrincipal(req, deps.config);
114
114
  if (deps.config.requirePrincipal && principal === undefined) {
115
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
115
+ sendPrincipalRefusal(res, req, deps.config);
116
116
  return;
117
117
  }
118
118
  const wakeSession = decodeURIComponent(url.split("/")[3]);
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, isOperator, explicitOperatorOk } from "../principal-gate.js";
2
+ import { gatedPrincipal, isOperator, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  export async function handleObservability(req, res, url, ctx) {
4
4
  const miss = { fell: false };
5
5
  await handleObservabilityBody(req, res, url, ctx, miss);
@@ -34,7 +34,7 @@ async function handleObservabilityBody(req, res, url, ctx, miss) {
34
34
  if (url === "/v1/usage" && req.method === "GET") {
35
35
  const principal = gatedPrincipal(req, deps.config);
36
36
  if (!principal) {
37
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
37
+ sendPrincipalRefusal(res, req, deps.config);
38
38
  return;
39
39
  }
40
40
  const windowSec = deps.config.costQuotaWindowSec;
@@ -1,5 +1,5 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
2
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { RETENTION_AUDIT_DEFAULT_LIMIT, RETENTION_AUDIT_MAX_LIMIT } from "../../plugins/retention-lane-store-sql.js";
4
4
  const HOLD_RE = /^\/v1\/ops\/retention\/holds\/([^/]*)$/;
5
5
  const AUDIT_PATH = "/v1/ops/retention/audit";
@@ -40,7 +40,7 @@ async function handleRetentionBody(req, res, url, ctx, miss) {
40
40
  }
41
41
  const principal = gatedPrincipal(req, deps.config);
42
42
  if (deps.config.requirePrincipal && !principal) {
43
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
43
+ sendPrincipalRefusal(res, req, deps.config);
44
44
  return;
45
45
  }
46
46
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
- import { gatedPrincipal } from "../principal-gate.js";
4
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
5
5
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
6
6
  export const RUN_MEMORY_CAPTURE_OPTOUT_RE = /^\/v1\/runs\/([^/]+)\/memory\/capture-optout$/;
7
7
  const BodySchema = z.object({ reason: z.string().min(1).max(500).optional() }).strict();
@@ -25,7 +25,7 @@ async function handleBody(req, res, url, ctx, miss) {
25
25
  }
26
26
  const principal = gatedPrincipal(req, deps.config);
27
27
  if (deps.config.requirePrincipal && principal === undefined) {
28
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
28
+ sendPrincipalRefusal(res, req, deps.config);
29
29
  return;
30
30
  }
31
31
  const taskId = m[1];