@sema-agent/server 7.3.0 → 7.4.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 (98) hide show
  1. package/USAGE.md +20 -0
  2. package/dist/approval-reconciler.d.ts +12 -5
  3. package/dist/boot/coordinators.js +3 -1
  4. package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
  5. package/dist/boot/deferred-sandbox-path-env.js +279 -0
  6. package/dist/boot/execution-env.js +11 -1
  7. package/dist/boot/lexical-path-env.d.ts +0 -4
  8. package/dist/boot/lexical-path-env.js +6 -34
  9. package/dist/boot/resolve-spec.js +50 -38
  10. package/dist/capabilities/center-prompts.js +4 -1
  11. package/dist/capabilities/oa-tools.d.ts +15 -0
  12. package/dist/capabilities/oa-tools.js +54 -0
  13. package/dist/config-types.d.ts +8 -1
  14. package/dist/config.js +9 -1
  15. package/dist/elicitation.js +5 -1
  16. package/dist/finance/cost-taxonomy.d.ts +34 -0
  17. package/dist/finance/cost-taxonomy.js +26 -0
  18. package/dist/hooks/hook-runner.js +32 -0
  19. package/dist/http/routes/diagnostics.d.ts +84 -0
  20. package/dist/http/routes/diagnostics.js +140 -0
  21. package/dist/http/routes/tasks.js +31 -10
  22. package/dist/http/server.d.ts +5 -0
  23. package/dist/http/server.js +6 -0
  24. package/dist/main.js +34 -3
  25. package/dist/observability/fail-open.d.ts +98 -0
  26. package/dist/observability/fail-open.js +216 -0
  27. package/dist/observability/prompt-manifest.d.ts +13 -0
  28. package/dist/observability/prompt-manifest.js +8 -0
  29. package/dist/plugins/approval-store-sql.d.ts +116 -0
  30. package/dist/plugins/approval-store-sql.js +151 -0
  31. package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
  32. package/dist/plugins/file-workflow-journal-store.js +12 -0
  33. package/dist/plugins/pg-approval-store.d.ts +9 -0
  34. package/dist/plugins/pg-approval-store.js +9 -0
  35. package/dist/plugins/pg-breaker-state.d.ts +8 -0
  36. package/dist/plugins/pg-breaker-state.js +8 -0
  37. package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
  38. package/dist/plugins/pg-checkpoint-store.js +10 -0
  39. package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
  40. package/dist/plugins/pg-file-snapshot-store.js +8 -0
  41. package/dist/plugins/pg-image-bake.d.ts +12 -0
  42. package/dist/plugins/pg-image-bake.js +11 -0
  43. package/dist/plugins/pg-image-index.d.ts +12 -0
  44. package/dist/plugins/pg-image-index.js +11 -0
  45. package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
  46. package/dist/plugins/pg-outcome-ledger.js +11 -0
  47. package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
  48. package/dist/plugins/pg-resume-anchor-store.js +7 -0
  49. package/dist/plugins/pg-run-store.d.ts +9 -0
  50. package/dist/plugins/pg-run-store.js +9 -0
  51. package/dist/plugins/pg-session-policy-store.d.ts +7 -0
  52. package/dist/plugins/pg-session-policy-store.js +7 -0
  53. package/dist/plugins/pg-session-store.d.ts +12 -0
  54. package/dist/plugins/pg-session-store.js +12 -0
  55. package/dist/plugins/pg-tool-result-store.d.ts +9 -0
  56. package/dist/plugins/pg-tool-result-store.js +9 -0
  57. package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
  58. package/dist/plugins/pg-workflow-journal-store.js +9 -0
  59. package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
  60. package/dist/plugins/pg-workflow-run-store.js +9 -0
  61. package/dist/plugins/tidb-approval-store.d.ts +8 -0
  62. package/dist/plugins/tidb-approval-store.js +8 -0
  63. package/dist/plugins/tidb-breaker-state.d.ts +7 -0
  64. package/dist/plugins/tidb-breaker-state.js +7 -0
  65. package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
  66. package/dist/plugins/tidb-checkpoint-store.js +9 -0
  67. package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
  68. package/dist/plugins/tidb-file-snapshot-store.js +8 -0
  69. package/dist/plugins/tidb-image-bake.d.ts +12 -0
  70. package/dist/plugins/tidb-image-bake.js +11 -0
  71. package/dist/plugins/tidb-image-index.d.ts +12 -0
  72. package/dist/plugins/tidb-image-index.js +11 -0
  73. package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
  74. package/dist/plugins/tidb-outcome-ledger.js +12 -0
  75. package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
  76. package/dist/plugins/tidb-resume-anchor-store.js +7 -0
  77. package/dist/plugins/tidb-run-store.d.ts +10 -0
  78. package/dist/plugins/tidb-run-store.js +9 -0
  79. package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
  80. package/dist/plugins/tidb-session-policy-store.js +7 -0
  81. package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
  82. package/dist/plugins/tidb-tool-result-store.js +10 -0
  83. package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
  84. package/dist/plugins/tidb-workflow-journal-store.js +9 -0
  85. package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
  86. package/dist/plugins/tidb-workflow-run-store.js +10 -0
  87. package/dist/plugins/workflow-journal-limits.d.ts +12 -0
  88. package/dist/plugins/workflow-journal-limits.js +12 -0
  89. package/dist/question.d.ts +18 -14
  90. package/dist/question.js +83 -34
  91. package/dist/sema-registry.d.ts +41 -0
  92. package/dist/sema-registry.js +40 -0
  93. package/dist/task-settings.d.ts +13 -12
  94. package/dist/task-settings.js +6 -6
  95. package/dist/tool-approval.js +15 -7
  96. package/dist/trace/project.d.ts +36 -1
  97. package/dist/trace/project.js +55 -2
  98. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { type AskQuestionRequest, type QuestionAnswer, type AskQuestion } from "@sema-agent/core";
1
+ import { type AskQuestionRequest, type OnQuestionOutcome, type QuestionAnswer, type AskQuestion } from "@sema-agent/core";
2
2
  /** A live question frame delivered to whoever tails this run's stream. `type` IS the SSE event name (mirrors the
3
3
  * named-event convention; the payload also carries `type` so a proxy that strips event names still works). The shell
4
4
  * renders `question` as a dialog and dismisses on `question_complete`. */
@@ -6,10 +6,10 @@ export interface QuestionFrame {
6
6
  type: "question" | "question_complete";
7
7
  questionId: string;
8
8
  /** "question" only: the model's structured questions, secret-redacted. UNTRUSTED-for-display (shell renders; never
9
- * re-feeds a model). Absent on an over-cap payload (the ask then headless-defaults). */
9
+ * re-feeds a model). Absent on an over-cap payload (the ask then reports unavailable — no frame is emitted at all). */
10
10
  questions?: AskQuestion[];
11
11
  /** "question_complete" only: whether the human answered (`answered`) or the ask was released unanswered
12
- * (`unanswered` — ttl/abort → the model got the headless default). Cosmetic dialog-dismiss. */
12
+ * (`unanswered` — ttl/abort → core was told nobody was reachable). Cosmetic dialog-dismiss. */
13
13
  outcome?: "answered" | "unanswered";
14
14
  /** Server-signed emit timestamp (additive, [2805]§五→[2806]). Server-minted side-frame ⇒ no core-stream
15
15
  * `eventId` to carry (absence is honest); this is the frame's own time coordinate. */
@@ -31,12 +31,14 @@ export interface QuestionRunContext {
31
31
  * background leg (emit appends to the durable events tail, readable whenever a client attaches). */
32
32
  deliverable?: () => boolean;
33
33
  }
34
+ /** The per-run question window the deployment owns (core caps nothing). Env: `QUESTION_MAX_CONCURRENT_PER_RUN` /
35
+ * `QUESTION_MAX_TOTAL_PER_RUN` / `QUESTION_TTL_MS` (see `ServiceConfig.questionThrottle`). */
34
36
  export interface QuestionThrottle {
35
- /** Max concurrent in-flight questions per run leg (parallel tool calls can each ask). Breach ⇒ headless default. */
37
+ /** Max concurrent in-flight questions per run leg (parallel tool calls can each ask). Breach ⇒ unavailable. */
36
38
  maxConcurrentPerRun: number;
37
- /** Max total questions one run leg surfaces to the human. Breach ⇒ headless default (flood defense). */
39
+ /** Max total questions one run leg surfaces to the human. Breach ⇒ unavailable (flood defense). */
38
40
  maxTotalPerRun: number;
39
- /** An unanswered question auto-releases (headless default) after this (ms) — the human walked away; don't hold core. */
41
+ /** An unanswered question auto-releases as unavailable after this (ms) — the human walked away; don't hold core. */
40
42
  ttlMs: number;
41
43
  }
42
44
  export declare const DEFAULT_QUESTION_THROTTLE: QuestionThrottle;
@@ -54,7 +56,7 @@ export declare function parseQuestionResponse(body: unknown): {
54
56
  * Coordinates AskUserQuestion HITL for the singleton runner. Process-local + same-replica (the pending map is in memory,
55
57
  * like ElicitationCoordinator / steerableRuns): a respond that lands on another replica finds nothing → 404. Present
56
58
  * (passed into `RunnerDeps.onQuestion` + the respond route) ONLY when `ASK_QUESTION_ENABLED` — absent ⇒ core mounts the
57
- * AskUserQuestion tool with the headless default (a run never hangs; the model just can't get a live answer).
59
+ * AskUserQuestion tool with no seam at all (`seam_absent` continuation; the model just can't get a live answer).
58
60
  */
59
61
  export declare class QuestionCoordinator {
60
62
  private readonly als;
@@ -63,12 +65,13 @@ export declare class QuestionCoordinator {
63
65
  private readonly throttle;
64
66
  constructor(throttle?: QuestionThrottle);
65
67
  /** Run `fn` with the per-run question context ambient. On exit, release any still-pending question for this run (a
66
- * live-only question cannot outlive its leg → headless default) and drop the run's counters (no leak). */
68
+ * live-only question cannot outlive its leg → unavailable) and drop the run's counters (no leak). */
67
69
  runWithContext<T>(ctx: QuestionRunContext, fn: () => Promise<T>): Promise<T>;
68
- /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved answer is what
69
- * the model sees. Headless default (`NO_HUMAN`) in every case with no live human, so the run never hangs. Arrow
70
- * property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
71
- question: (req: AskQuestionRequest, signal?: AbortSignal) => Promise<QuestionAnswer>;
70
+ /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved outcome is what
71
+ * core acts on a {@link QuestionAnswer} ONLY when a human really answered, otherwise `{kind:"unavailable"}` (core
72
+ * then parks on a durable leg / continues with the `declined_unavailable` card on a non-durable one; either way the
73
+ * run never hangs). Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
74
+ question: (req: AskQuestionRequest, signal?: AbortSignal) => Promise<OnQuestionOutcome>;
72
75
  /** `POST /v1/questions/:id/respond` — resolve a parked question with the shell's answer. Owner-gated with a 404 (no
73
76
  * existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404. Returns the
74
77
  * HTTP {status, body}; the HTTP layer owns auth (gatedPrincipal + REQUIRE_PRINCIPAL) before calling. */
@@ -83,9 +86,10 @@ export declare class QuestionCoordinator {
83
86
  * resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
84
87
  * ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
85
88
  * 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
86
- * 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
89
+ * 只判 ALS 会把一条谁也收不到的问题判成 allow——挂满 ttl 后以 unavailable 结算,非 durable 形 core
90
+ * 就此合成续跑,活人再没机会答;而 park 才是它该走的腿。
87
91
  * 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
88
- * fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
92
+ * fail-closed 到 park(park 可由运维补答,续跑掉的问题不可回收)。 */
89
93
  hasLiveContext(): boolean;
90
94
  private countersFor;
91
95
  }
package/dist/question.js CHANGED
@@ -15,29 +15,39 @@
15
15
  * (the HTTP boundary) and REDACTS secrets from the model-authored QUESTION text before it is persisted/streamed (the
16
16
  * same redact-at-write contract as elicitation's `message`/tool outputs — the question is shown to a human, never
17
17
  * re-fed to a model, so no `delimitUntrusted` is needed on it). Deployment responsibilities core delegates: THROTTLE
18
- * (bound concurrency/total/ttl per run — an over-eager agent could flood the human) and no-live-human FALLBACK (a leg
19
- * with no stream returns the headless "no answer" default so the model proceeds instead of hanging).
18
+ * (bound concurrency/total/ttl per run — an over-eager agent could flood the human).
19
+ *
20
+ * 🔴 VERDICT vs LANDING are separate jobs (#166 裁1). This coordinator is the deployment's HONEST reporter of one fact:
21
+ * "no human answered this". Every arm with no real human answer — no run context, the run aborted, the throttle window
22
+ * is spent, the question cannot be presented, the TTL expired (incl. the run-exit sweep) — returns core's
23
+ * `{kind:"unavailable"}`. It NEVER synthesizes an empty answer set: an empty `{answers:[]}` reads to core as "a human
24
+ * was here and picked nothing", which on a durable deployment consumes the pending approval and hands the model a
25
+ * fabricated human verdict instead of parking the question where an operator can still answer it. Choosing the landing
26
+ * is CORE's job: a durable leg parks a checkpoint (operator answers later via decide), a non-durable leg continues with
27
+ * the `declined_unavailable` synthetic-continuation card (the model is told nobody is reachable and proceeds on its own
28
+ * judgment — a run with no human still never hangs). Only a REAL human answer returns a {@link QuestionAnswer}.
20
29
  *
21
30
  * ROUTING mirrors elicitation: `onQuestion` lives on the SINGLETON runner's `RunnerDeps` (the request carries no taskId),
22
31
  * so the run leg establishes a per-run AsyncLocalStorage context (taskId throttle key, owner respond-gate, `emit`
23
- * closure). `node:async_hooks` propagates it through every await; no context ⇒ headless default (never a hang).
32
+ * closure). `node:async_hooks` propagates it through every await; no context ⇒ unavailable (never a hang).
24
33
  */
25
34
  import { AsyncLocalStorage } from "node:async_hooks";
26
35
  import { uuidv7 } from "@sema-agent/core";
27
36
  import { redactDeep } from "./trace/redact.js";
37
+ import { recordFailOpen } from "./observability/fail-open.js";
28
38
  /** Size bound on the model-authored questions payload before it is persisted to the durable log + streamed (parity
29
39
  * with elicitation's MAX_ELICIT_* — an over-large questions blob amplifies into the socket/log). Over the cap ⇒ the
30
- * questions are dropped and the ask headless-defaults (a question the human can't see can't be answered). */
40
+ * questions are dropped and the ask reports unavailable (a question the human can't see can't be answered). */
31
41
  const MAX_QUESTIONS_BYTES = 16384;
32
42
  export const DEFAULT_QUESTION_THROTTLE = {
33
43
  maxConcurrentPerRun: 2,
34
44
  maxTotalPerRun: 20,
35
45
  ttlMs: 5 * 60_000,
36
46
  };
37
- /** The headless "no live human answered" result an EMPTY answer set (no question got a selection). core's
38
- * AskUserQuestion tool renders this to the model as "no option selected" so it proceeds with its own judgment instead
39
- * of hanging (parity with the `onQuestion===undefined` headless default a run with no human never blocks). */
40
- const NO_HUMAN = { answers: [] };
47
+ /** The honest "nobody answered this" outcome. Frozen + shared: it is pure data core reads through
48
+ * `classifyQuestionOutcome`, and it must never be confused with an empty ANSWER (which claims a human chose nothing
49
+ * and, on a durable leg, spends the approval that park would have preserved). */
50
+ const UNAVAILABLE = Object.freeze({ kind: "unavailable" });
41
51
  /** Validate the respond body = core's {@link QuestionAnswer}. `selected`/`note` are OPAQUE passthrough (core fences
42
52
  * them: `selected ⊆ options`, `note` untrusted-fenced) — we validate only the outer SHAPE (answers[] of {header,
43
53
  * selected:string[], note?}), never the option semantics. */
@@ -65,7 +75,7 @@ export function parseQuestionResponse(body) {
65
75
  }
66
76
  /** redactDeep + size-bound the UNTRUSTED model-authored questions before they are persisted/streamed (a question's
67
77
  * free-text — question/header/option label+description — can echo a secret from the model's context). Over the byte
68
- * cap OR unserializable ⇒ undefined (drop; the ask headless-defaults). */
78
+ * cap OR unserializable ⇒ undefined (drop; the ask reports unavailable). */
69
79
  function boundQuestions(questions) {
70
80
  const redacted = redactDeep(questions);
71
81
  try {
@@ -81,7 +91,7 @@ function boundQuestions(questions) {
81
91
  * Coordinates AskUserQuestion HITL for the singleton runner. Process-local + same-replica (the pending map is in memory,
82
92
  * like ElicitationCoordinator / steerableRuns): a respond that lands on another replica finds nothing → 404. Present
83
93
  * (passed into `RunnerDeps.onQuestion` + the respond route) ONLY when `ASK_QUESTION_ENABLED` — absent ⇒ core mounts the
84
- * AskUserQuestion tool with the headless default (a run never hangs; the model just can't get a live answer).
94
+ * AskUserQuestion tool with no seam at all (`seam_absent` continuation; the model just can't get a live answer).
85
95
  */
86
96
  export class QuestionCoordinator {
87
97
  als = new AsyncLocalStorage();
@@ -92,7 +102,7 @@ export class QuestionCoordinator {
92
102
  this.throttle = throttle;
93
103
  }
94
104
  /** Run `fn` with the per-run question context ambient. On exit, release any still-pending question for this run (a
95
- * live-only question cannot outlive its leg → headless default) and drop the run's counters (no leak). */
105
+ * live-only question cannot outlive its leg → unavailable) and drop the run's counters (no leak). */
96
106
  runWithContext(ctx, fn) {
97
107
  return this.als.run(ctx, async () => {
98
108
  try {
@@ -101,31 +111,34 @@ export class QuestionCoordinator {
101
111
  finally {
102
112
  for (const [, p] of [...this.pending]) {
103
113
  if (p.taskId === ctx.taskId)
104
- p.settle(NO_HUMAN, "unanswered");
114
+ p.settle(UNAVAILABLE, "unanswered");
105
115
  }
106
116
  this.counters.delete(ctx.taskId);
107
117
  }
108
118
  });
109
119
  }
110
- /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved answer is what
111
- * the model sees. Headless default (`NO_HUMAN`) in every case with no live human, so the run never hangs. Arrow
112
- * property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
120
+ /** `RunnerDeps.onQuestion`. core calls this when the agent's AskUserQuestion tool fires; the resolved outcome is what
121
+ * core acts on a {@link QuestionAnswer} ONLY when a human really answered, otherwise `{kind:"unavailable"}` (core
122
+ * then parks on a durable leg / continues with the `declined_unavailable` card on a non-durable one; either way the
123
+ * run never hangs). Arrow property so it can be passed as `onQuestion: coordinator.question` with `this` bound. */
113
124
  question = async (req, signal) => {
114
125
  const ctx = this.als.getStore();
115
126
  // No run context (a leg not wrapped by runWithContext — synchronous /v1/tasks, verify/cascade, or a leader sub-task
116
- // with no live stream) ⇒ headless default. Sub-tasks run in the parent's ALS frame → a child's question inherits the
127
+ // with no live stream) ⇒ nobody to ask. Sub-tasks run in the parent's ALS frame → a child's question inherits the
117
128
  // top-level run's ctx (one human, one stream, one throttle budget), same as elicitation.
118
129
  if (!ctx)
119
- return NO_HUMAN;
130
+ return UNAVAILABLE;
131
+ // Aborted (call signal or the run's own): whatever comes back has no consumer, so claiming a human answered is a lie.
120
132
  if (signal?.aborted || ctx.abortSignal?.aborted)
121
- return NO_HUMAN;
122
- // THROTTLE (service's job — core imposes none): bound concurrency + total. Breach ⇒ headless default.
133
+ return UNAVAILABLE;
134
+ // THROTTLE (service's job — core imposes none): bound concurrency + total. Breach ⇒ unavailable, never a denial and
135
+ // never a fabricated answer — a spent window is a "not now", which is exactly what park/continue is for.
123
136
  const rc = this.countersFor(ctx.taskId);
124
137
  if (rc.inflight >= this.throttle.maxConcurrentPerRun || rc.total >= this.throttle.maxTotalPerRun)
125
- return NO_HUMAN;
138
+ return UNAVAILABLE;
126
139
  const questions = boundQuestions(req.questions);
127
140
  if (!questions)
128
- return NO_HUMAN; // over-cap / unserializable → can't show it → headless
141
+ return UNAVAILABLE; // over-cap / unserializable → can't show it → nobody can answer it
129
142
  const id = uuidv7();
130
143
  rc.inflight += 1; // reserve the concurrency slot; total charged only on a successful emit
131
144
  let done = false;
@@ -147,8 +160,10 @@ export class QuestionCoordinator {
147
160
  resolveAnswer(a);
148
161
  };
149
162
  let lastOutcome = "unanswered";
150
- const onAbort = () => settle(NO_HUMAN, "unanswered");
151
- timer = setTimeout(() => settle(NO_HUMAN, "unanswered"), this.throttle.ttlMs);
163
+ const onAbort = () => settle(UNAVAILABLE, "unanswered");
164
+ // TTL: the human walked away. Release as unavailable — core decides whether that parks (durable, operator can still
165
+ // answer) or continues (non-durable); holding core past the window is not one of the choices.
166
+ timer = setTimeout(() => settle(UNAVAILABLE, "unanswered"), this.throttle.ttlMs);
152
167
  timer.unref?.();
153
168
  signal?.addEventListener("abort", onAbort, { once: true });
154
169
  ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
@@ -156,20 +171,53 @@ export class QuestionCoordinator {
156
171
  this.pending.set(id, { settle, owner: ctx.owner, taskId: ctx.taskId });
157
172
  // Emit the OPEN frame AWAITED so its durable append is ORDERED ahead of the answer/completion/terminal appends (a
158
173
  // fire-and-forget append can commit out of seq order and be skipped by the monotonic events-tail cursor). A FAILED
159
- // emit headless-defaults (nobody can answer) and must NOT burn the per-run total.
160
- try {
161
- await ctx.emit({ type: "question", questionId: id, questions, serverNowMs: Date.now() });
174
+ // emit settles unavailable (nobody can answer a frame that never arrived) and must NOT burn the per-run total.
175
+ // 🔴 RACED against the settlement (codex 复审 R2-high): a STALLED delivery (a durable append that never settles, a
176
+ // half-open socket) is a third shape next to ok/threw, and a bare `await ctx.emit` on it outlives TTL *and* abort
177
+ // settle would resolve `answerP` while this frame is still parked on the emit, so `question()` never returns, core's
178
+ // callTool never unblocks and the leg cannot even reach its own cleanup. "The window always releases" has to hold
179
+ // for the stalled shape too, so the await ends at whichever comes first. Ordering is unchanged on the normal path
180
+ // (the settlement is still pending there, so the emit is still fully awaited).
181
+ // The loser of the race keeps its own handlers — an emit that rejects AFTER the race must not surface as an
182
+ // unhandled rejection.
183
+ const emitted = (async () => ctx.emit({ type: "question", questionId: id, questions, serverNowMs: Date.now() }))()
184
+ .then(() => "ok", () => "failed");
185
+ const delivery = await Promise.race([emitted, answerP.then(() => "stalled")]);
186
+ if (delivery === "ok") {
162
187
  rc.total += 1;
163
188
  }
164
- catch {
165
- settle(NO_HUMAN, "unanswered");
189
+ else if (delivery === "failed") {
190
+ // #157(codex 复审 MEDIUM,2026-08-07):投递失败 ⇒ 人在环这道门被**一次故障**跳过,模型自己往下判。
191
+ // 「绝不把 run 挂死」是刻意的产品姿态,所以方向不改;但它是 P-DEBT 不是合法兜底——债要在遥测里显形。
192
+ // #166 后本臂结算成 unavailable(而非空答):留痕语义不变(一次投递故障仍可能让人在环被跳过——非
193
+ // durable 形 core 合成续跑),durable 形则改由 core park,债的方向与本记账口径一致。
194
+ recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
195
+ settle(UNAVAILABLE, "unanswered");
166
196
  }
197
+ // delivery==="stalled":窗已由 TTL/abort/清扫结算,这一问就此让位;账在下面那个迟到臂里补(投递还没
198
+ // 判出成败,现在既不能计费也不能记债)。
167
199
  const answer = await answerP;
168
200
  // Completion breadcrumb (dialog dismiss) — FIRE-AND-FORGET so a slow durable append can never delay returning the
169
- // answer to core. Best-effort + ordering-uncritical (the shell already has the answer via respond).
170
- void Promise.resolve()
171
- .then(() => ctx.emit({ type: "question_complete", questionId: id, outcome: lastOutcome, serverNowMs: Date.now() }))
172
- .catch(() => undefined);
201
+ // answer to core. Best-effort (the shell already has the answer via respond), but CHAINED BEHIND the open frame and
202
+ // BRANCHED on how that frame ended (codex 复审 R3):
203
+ // · 送达(含**迟到**送达) 人真看见了这张卡:补烧总量额度(否则「反复悬挂再落地」是一条绕过刷屏帽的
204
+ // 路子),再发消卡帧把它消解掉;
205
+ // · 投递失败(含**迟到**失败)⇒ 没有卡可消解,发消卡帧只会在流里留一条无主帧;此时补记那条 P-DEBT
206
+ // 留痕(同步臂已经记过的不会重复:那条腿的 delivery 不是 stalled)。
207
+ void emitted
208
+ .then((result) => {
209
+ if (result === "failed") {
210
+ if (delivery === "stalled")
211
+ recordFailOpen("server.question.open-frame-undelivered", `question=${id}`);
212
+ return undefined;
213
+ }
214
+ if (delivery === "stalled")
215
+ rc.total += 1;
216
+ return ctx.emit({ type: "question_complete", questionId: id, outcome: lastOutcome, serverNowMs: Date.now() });
217
+ })
218
+ // #157:F 类兜底(丢的是收尾渲染,不是答案)——合法,但必须留痕,否则"面包屑成片消失"只能靠
219
+ // 用户报"对话框不消解"发现。
220
+ .catch(() => recordFailOpen("server.question.complete-breadcrumb-dropped", `question=${id}`));
173
221
  return answer;
174
222
  };
175
223
  /** `POST /v1/questions/:id/respond` — resolve a parked question with the shell's answer. Owner-gated with a 404 (no
@@ -197,9 +245,10 @@ export class QuestionCoordinator {
197
245
  * resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
198
246
  * ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
199
247
  * 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
200
- * 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
248
+ * 只判 ALS 会把一条谁也收不到的问题判成 allow——挂满 ttl 后以 unavailable 结算,非 durable 形 core
249
+ * 就此合成续跑,活人再没机会答;而 park 才是它该走的腿。
201
250
  * 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
202
- * fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
251
+ * fail-closed 到 park(park 可由运维补答,续跑掉的问题不可回收)。 */
203
252
  hasLiveContext() {
204
253
  const ctx = this.als.getStore();
205
254
  if (ctx === undefined)
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Sema-registry adapter — pull the effective config from sema-registry and apply it OVER the
3
+ * env-derived defaults (env = fallback, center = override). Follows the "universal internal
4
+ * schema → translate at the boundary" pattern, with our security boundary kept:
5
+ *
6
+ * - the CENTER owns the LOGICAL config: the model roster (names/capabilities/tier), the role map,
7
+ * and team templates;
8
+ * - the SERVICE env still owns the SECRETS: API keys stay in env (the center never stores a secret);
9
+ * keys are per-model via `apiKeyEnv` → `config.modelApiKeyEnv` → the spec's `getApiKeyAndHeaders`
10
+ * (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
11
+ *
12
+ * Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
13
+ * core brain honors model.baseUrl, absent = "" = boot-env endpoint).
14
+ *
15
+ * Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
16
+ * TODO, which went stale when `mutateInPlace` landed):
17
+ * - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
18
+ * `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
19
+ * reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
20
+ * call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
21
+ * next task with no restart.
22
+ * - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
23
+ * at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
24
+ * - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
25
+ * (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
26
+ * at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
27
+ * restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
28
+ * gateway silently on the rate-limit path.
29
+ * - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
30
+ *
31
+ * (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives in
32
+ * `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig application /
33
+ * restart-signal detection / skills+MCP consumption). Every symbol below is re-exported UNCHANGED —
34
+ * existing `from "./sema-registry.js"` / `from "../sema-registry.js"` imports need zero changes.
35
+ */
36
+ export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./config-center/http-client.js";
37
+ export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./config-center/apply-effective.js";
38
+ export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./config-center/restart-signal.js";
39
+ export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./config-center/skills-mcp.js";
40
+ export type { CenterSkillManifest, CenterMcpServer, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./config-center/types.js";
41
+ //# sourceMappingURL=sema-registry.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Sema-registry adapter — pull the effective config from sema-registry and apply it OVER the
3
+ * env-derived defaults (env = fallback, center = override). Follows the "universal internal
4
+ * schema → translate at the boundary" pattern, with our security boundary kept:
5
+ *
6
+ * - the CENTER owns the LOGICAL config: the model roster (names/capabilities/tier), the role map,
7
+ * and team templates;
8
+ * - the SERVICE env still owns the SECRETS: API keys stay in env (the center never stores a secret);
9
+ * keys are per-model via `apiKeyEnv` → `config.modelApiKeyEnv` → the spec's `getApiKeyAndHeaders`
10
+ * (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
11
+ *
12
+ * Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
13
+ * core brain honors model.baseUrl, absent = "" = boot-env endpoint).
14
+ *
15
+ * Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
16
+ * TODO, which went stale when `mutateInPlace` landed):
17
+ * - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
18
+ * `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
19
+ * reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
20
+ * call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
21
+ * next task with no restart.
22
+ * - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
23
+ * at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
24
+ * - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
25
+ * (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
26
+ * at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
27
+ * restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
28
+ * gateway silently on the rate-limit path.
29
+ * - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
30
+ *
31
+ * (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives in
32
+ * `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig application /
33
+ * restart-signal detection / skills+MCP consumption). Every symbol below is re-exported UNCHANGED —
34
+ * existing `from "./sema-registry.js"` / `from "../sema-registry.js"` imports need zero changes.
35
+ */
36
+ export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./config-center/http-client.js";
37
+ export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./config-center/apply-effective.js";
38
+ export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./config-center/restart-signal.js";
39
+ export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./config-center/skills-mcp.js";
40
+ //# sourceMappingURL=sema-registry.js.map
@@ -149,7 +149,7 @@ export declare function providerDropsAppend(provider: PromptProvider | undefined
149
149
  export declare function acceptAppendSystemPrompt(v: unknown, warn?: (detail: string) => void, packDropsAppend?: boolean): string | undefined;
150
150
  /** [1248]②/codex F2 — what the workflow ask leg needs from the deployment, on EVERY lane. It is name-keyed and
151
151
  * fs-independent, so it never had the lane split {@link FsWriteGateWiring} carried (that split is itself gone
152
- * since #156 — the sandbox lanes now wire a lexical-path gate instead of no gate). */
152
+ * since #156 — the sandbox lanes wire a gate of their own shape instead of no gate). */
153
153
  export interface WorkflowGateWiring {
154
154
  /** The session "don't ask again" probe (approvalExemptionStore.has, canonical toolName key) — same store and
155
155
  * key space as the fs-write gate's probe; one remember="session" grant serves both. */
@@ -162,22 +162,23 @@ export interface WorkflowGateWiring {
162
162
  * (real `exists`/`canonicalPath`/`readLink` fs access — read in dist, fs-write-gate-policy.js), so on a
163
163
  * HOST-SEMANTICS lane (`REMOTE_EXEC` unset or "host") the env MUST be the fs the hand tools write.
164
164
  *
165
- * #156 — the sandbox lanes (e2b/k8s/ssh/adb/local-docker) now wire a gate TOO, but a different-shaped one: the
165
+ * #156/#165 — the sandbox lanes (e2b/k8s/ssh/adb/local-docker) wire a gate TOO, but a different-shaped one: the
166
166
  * tools run OFF this box and the per-task sandbox env is minted inside core (executionEnvFactory) AFTER spec
167
167
  * build, so a worker-local env would adjudicate against the WRONG fs (a wrong ALLOW is worse than no gate — that
168
- * is why [816] left those lanes gate-less). The transitional shape instead supplies a `LexicalPathExecutionEnv`
169
- * (`src/boot/lexical-path-env.ts`): NO fs is consulted at all, absolute paths are judged by lexical
170
- * normalization and everything else fails closed to `ask`. Its residual surface (symlink form) and the endgame
171
- * seam (core [2751] read-only env face on HookToolContext) are documented on that module. */
168
+ * is why [816] left those lanes gate-less). They supply a `DeferredSandboxPathEnv`
169
+ * (`src/boot/deferred-sandbox-path-env.ts`) instead: a proxy that forwards the fs READ primitives to the task's
170
+ * REAL sandbox env at adjudication time (a per-session slot the factory decorator fills), keeping only
171
+ * `absolutePath` lexical so no relative path is resolved against the wrong base. Empty slot ⇒ fail-closed. */
172
172
  export interface FsWriteGateWiring {
173
- /** The env the gate canonicalizes against — the worker host env on the host lane, the fs-less
174
- * `LexicalPathExecutionEnv` on the sandbox lanes (#156). */
173
+ /** The env the gate canonicalizes against — the worker host env on the host lane, the
174
+ * `DeferredSandboxPathEnv` proxy over the task's real sandbox env on the sandbox lanes (#165). */
175
175
  env: ExecutionEnv;
176
176
  /** The task's working directory: the factory's `rootPath` (relative-path base) AND the acceptEdits accept domain.
177
- * ABSENT on the sandbox lanes (#156): the sandbox cwd is not knowable at spec time and inventing one would mint
178
- * a bogus auto-allow domain. With it absent the gate gets no `rootPath` and no `acceptDirs`, so relative paths
179
- * fail to resolve (⇒ ask) and `acceptEdits` degrades to the `default` arm the same fail-safe direction as the
180
- * host lane's never-created sentinel dir (resolve-spec 修5). */
177
+ * ABSENT on the sandbox lanes (#156/#165): the sandbox cwd is the ENGINE's tracked cwd, not the env's own, so
178
+ * neither spec time nor the env proxy can supply the right base inventing one would mint a bogus auto-allow
179
+ * domain. With it absent the gate gets no `rootPath` and no `acceptDirs`, so relative paths fail to resolve
180
+ * (⇒ ask) and `acceptEdits` degrades to the `default` arm — the same fail-safe direction as the host lane's
181
+ * never-created sentinel dir (resolve-spec 修5). */
181
182
  cwd?: string;
182
183
  /** The session scratchpad dir (envFacts.scratchpadDir, [820]③) — writes there are always auto-allowed. */
183
184
  scratchpadDir?: string;
@@ -395,9 +395,9 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
395
395
  policies.push(createAskListPolicy(ask));
396
396
  // The mode-derived fs-write ask gate (core 1.290 factory — real shape read in dist: {env, rootPath?, acceptDirs?,
397
397
  // exemptDirs?, defaultWrite}, gates Write/Edit/NotebookEdit, exempt→accept→default layering, canonicalize inside
398
- // the given env, only ever allow/ask). undefined without wiring (see FsWriteGateWiring). #156: the sandbox lanes
399
- // supply wiring too — same factory, a lexical (fs-less) env and no cwd; the shape difference lives entirely in
400
- // what `resolve-spec.ts` puts in the wiring, not in a second code path here.
398
+ // the given env, only ever allow/ask). undefined without wiring (see FsWriteGateWiring). #156/#165: the sandbox
399
+ // lanes supply wiring too — same factory, a deferred real-env proxy and no cwd; the shape difference lives
400
+ // entirely in what `resolve-spec.ts` puts in the wiring, not in a second code path here.
401
401
  // ③ (core 1.295): the sensitive-path DENY policy (patterns from the wiring — config SENSITIVE_WRITE_PATTERNS,
402
402
  // default = core's RECOMMENDED set) composes into the SAME fold, deny-wins — core semantics deny/"safety", so
403
403
  // neither the gate's own allow legs (exemptDirs/acceptDirs/isExempt session exemption) nor a settings allow can
@@ -406,9 +406,9 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
406
406
  const fsWriteGate = (acceptCwd) => {
407
407
  if (!gate)
408
408
  return undefined;
409
- // #156: `cwd` is optional (absent on the sandbox lanes). Omitting `rootPath` makes core leave a relative
410
- // target relative → its lexical env cannot resolve it → ask; omitting `acceptDirs` means acceptEdits mints
411
- // no auto-allow domain at all. Both are the intended fail-safe, NOT a degraded copy of the host arm.
409
+ // #156/#165: `cwd` is optional (absent on the sandbox lanes). Omitting `rootPath` makes core leave a relative
410
+ // target relative → the sandbox proxy's lexical `absolutePath` refuses it → ask; omitting `acceptDirs` means
411
+ // acceptEdits mints no auto-allow domain at all. Both are the intended fail-safe, NOT a degraded host arm.
412
412
  const gatePolicy = createFsWriteGatePolicy({
413
413
  env: gate.env,
414
414
  ...(gate.cwd !== undefined ? { rootPath: gate.cwd } : {}),
@@ -156,10 +156,18 @@ function boundArgs(args) {
156
156
  }
157
157
  /** #151 车3 刀 3b:`AskRequest.boundInputHash` 的**边界窄读**(车5 §9 C2 的对账 join 键)。
158
158
  *
159
- * 🔴 树上的 core d.ts(5.13.0)**没有这个键**(全仓 0 命中),字段由 core 认领、随预发/终版到货。
160
- * 窄读让编译与行为解耦:今天恒回 `null`(= 该行结构上永不满足收敛器判据 1,`unmatchableNoHash` 响亮化,
161
- * 这是设计要的 fail-safe),core 供值后同一行代码自然点亮。非字符串/空串一律按缺席处置 —— 一个形不对的
162
- * hash 比没有 hash 更危险(它会让「硬相等」这道门在一个垃圾值上偶然成立)。 */
159
+ * **已点亮**(#164 翻真验证车,2026-08-07 亲验 core 5.15.0):`AskRequest.boundInputHash` 现在既在类型面
160
+ * (`core/tool-policy.d.ts:83`,`readonly boundInputHash?: string`)也在真码面(`core/tool-policy.js:610`:
161
+ * `onAsk({ ...req, boundInputHash: boundInputHashOf(presented.value), args: approverView.value })`)——
162
+ * 每一只经 `resolveAsk` ask 都带值。窄读当初就是为这一刻写的:**同一行代码**不改而自然点亮,收敛器
163
+ * 判据 1 从「结构上恒不命中」变成会命中,`unmatchableNoHash` 从「响亮化的盲区」退回它该有的边缘含义。
164
+ * (旧注写的「树上 core 5.13.0 没有这个键 ⇒ 今天恒回 null」自 core 5.15.0 提货起即过期,勿据以判断。)
165
+ *
166
+ * 🔴 保留窄读而不改 import 的理由不变:`boundInputHashOf` 至今**不在 core 的公开导出面**上
167
+ * (package `exports` 只有 `.` / `./bench` / `./fixtures`),server 因此既算不出也不该算这个摘要 ——
168
+ * 它的职责恒是「原样透传落列」。非字符串/空串一律按缺席处置:一个形不对的 hash 比没有 hash 更危险
169
+ * (它会让「硬相等」这道门在一个垃圾值上偶然成立)。钉:test/stream-approval-on-e2e.test.ts 件4
170
+ * (正控 / 缺席 / 空串三臂 + 收敛器 `unmatchableNoHash` 归零的非零对照)。 */
163
171
  function readBoundInputHash(req) {
164
172
  if (req === null || typeof req !== "object")
165
173
  return null;
@@ -772,9 +780,9 @@ export class ToolApprovalCoordinator {
772
780
  // (未打 tag),按 SCHEMA POLICY drop-and-recreate,不给一个从未出厂的形状留兼容层(§6.2 裁定 +
773
781
  // 本仓「不做临时方案」纪律)。
774
782
  cardJson: buildApprovalCardEnvelope(id, card),
775
- // 车5 §9 C2 的对账 join 键。**窄读**(`AskRequest.boundInputHash` 在树上的 core 5.13.0 里 0 命中,
776
- // 字段由 core 认领、待 pre/终版到货)——有则存、无则 null;缺席 ⇒ 收敛器判据 1 结构上不命中
777
- // (`unmatchableNoHash` 响亮化),这是**设计要的** fail-safe,不是降级(禁「能取到时才比」)。
783
+ // 车5 §9 C2 的对账 join 键。**窄读**(顶注:core 5.15.0 起真有值,窄读的理由改为「摘要函数
784
+ // 不在 core 公开导出面,server 只透传不重算」)——有则存、无则 null;缺席 ⇒ 收敛器判据 1
785
+ // 结构上不命中(`unmatchableNoHash` 计数),这是**设计要的** fail-safe,不是降级(禁「能取到时才比」)。
778
786
  boundInputHash: readBoundInputHash(req),
779
787
  schemaVersion: 1,
780
788
  expiresAtMs,
@@ -270,19 +270,54 @@ export declare function workspaceChangedEventData(ev: {
270
270
  * resume 账本)都是租户可读面,而 operator 投影是 #154 的独立大件——按 core 写死的失败方向,**没有投影
271
271
  * 就不得披露**,所以本批先把整段剥掉止泄(此前 sync 腿的裸 catch-all 会把它连同 `lockedConfig` /
272
272
  * `compliance` / `memoryAdmission` / `retention` 四个治理面在场位原样透传给任意订阅者)。
273
- * 哪天 operator 投影落地,它是**另一个**构造器(按调用方身份分叉),不是往这里加键。
273
+ * operator 投影是**另一个**构造器({@link wiringManifestOperatorEventData},live 腿按调用方身份挑),
274
+ * 不是往这里加键 —— 两条 durable 腿没有调用方身份可挑,所以它们恒用本构造器。
274
275
  * ⚠️ 剥段还必须连**对该段取的哈希**一起剥:`configFingerprint` 同样不投影,理由见下方它那一行的旁注
275
276
  * (无盐哈希 + 4 布尔 = 16 组合的谕示器,把「结构上藏起来」的段原样还原出来)。
276
277
  *
277
278
  * 其余段逐键挑(禁 `{...manifest}`:1.72 task_progress 教训——未来 core 加段必须由人显式处置,而不是
278
279
  * 顺着 spread 裸上线,尤其当新段可能又是一个 audience 受限面时)。所有值都是引擎派生的闭枚举/布尔/
279
280
  * 有限数/不透明摘要串,**非用户内容 ⇒ 不脱敏**;畸形值一律按缺席处理(不铸假值,与 compacted 同规)。
281
+ *
282
+ * 🔴 **未标签面的准入裁定逐面记录在此,缺省不是「放行」**(#154 裁定 C)。core 只给 `governance` 标了
283
+ * audience,其余段没有标签 —— 没标签**不等于**可以默认投给租户,每一段都要有一次人的裁定,写在这里:
284
+ * · `interaction.posture`(三值:interactive / headless / absent)—— **裁:租户可见**。它是部署形的
285
+ * 自述(这条腿有没有人能答问题),消费端据此决定渲不渲交互面;不是治理秘密,也不构成任何治理位的
286
+ * 侧信道(它不参与 governance 那 4 个布尔的还原)。
287
+ * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
288
+ * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来。
280
289
  */
281
290
  export declare function wiringManifestEventData(ev: {
282
291
  manifest?: unknown;
283
292
  eventId?: unknown;
284
293
  parentToolCallId?: unknown;
285
294
  }): Record<string, unknown>;
295
+ /**
296
+ * `wiring_manifest` 的 **operator 投影**(#154 件一)—— {@link wiringManifestEventData} 的租户形加上
297
+ * `governance` 段与 core 的真 `configFingerprint`。
298
+ *
299
+ * 🔴 分叉发生在**读口**,不在账本:两条 durable 腿(ledger-sink / resume)恒存剥后形,因为账本写的那一刻
300
+ * 不知道未来的读者是谁(`GET /v1/runs/:id/events` 任何 run owner 都读得到),往账本存全量 = 把 operator-only
301
+ * 面写进租户可读介质;存两份则是双写漂移源。所以只有 live 腿在**知道调用方身份**的那一刻挑构造器。
302
+ * 代价是 operator 看不到历史腿的 governance 段 —— 治理四布尔是部署形事实(不随 leg 漂),静态自检面
303
+ * (`GET /v1/diagnostics/wiring`)随时给出当前真值。
304
+ *
305
+ * 准入判据是 `explicitOperatorOk`(**不是** `isOperator`):空 `OPERATOR_PRINCIPALS` 必须是「没有人是
306
+ * operator」,不能是「所有人都是」—— 后者会让一个没配 operator 名单的部署把治理面发给每一个订阅者。
307
+ *
308
+ * `configFingerprint` 只在本构造器里透传:它是 `sha256(canonicalize(manifest 去 leg/去两个 provenance))`
309
+ * 前 16 hex,**无盐**且输入含整个 governance 段,而 governance 只有 4 个布尔 ⇒ 16 种组合。租户手里已有其余
310
+ * 全部哈希输入,发给租户等于把剥掉的四位原样还原(谕示器);发给 operator 无损 —— 同一份产物里四布尔本就
311
+ * 在场。
312
+ *
313
+ * governance **逐键挑**(禁 `{...governance}`):core 往这个段里加第五键时必须由人显式处置,而不是顺着
314
+ * spread 裸上线 —— 这个段的每一键都是部署治理面,新键的默认待遇必须是「不发」。
315
+ */
316
+ export declare function wiringManifestOperatorEventData(ev: {
317
+ manifest?: unknown;
318
+ eventId?: unknown;
319
+ parentToolCallId?: unknown;
320
+ }): Record<string, unknown>;
286
321
  /**
287
322
  * `human_input`(core 5.14.0 design/171 新 `TaskEvent` 臂)—— 「谁把什么喂进了这条 run」的生命周期账本
288
323
  * 帧(objective / 实时 steer / nextTurn / parked-steer 续跑 / wake 五个载体共用一个投影点)。