@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.
- package/USAGE.md +20 -0
- package/dist/approval-reconciler.d.ts +12 -5
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
- package/dist/boot/deferred-sandbox-path-env.js +279 -0
- package/dist/boot/execution-env.js +11 -1
- package/dist/boot/lexical-path-env.d.ts +0 -4
- package/dist/boot/lexical-path-env.js +6 -34
- package/dist/boot/resolve-spec.js +50 -38
- package/dist/capabilities/center-prompts.js +4 -1
- package/dist/capabilities/oa-tools.d.ts +15 -0
- package/dist/capabilities/oa-tools.js +54 -0
- package/dist/config-types.d.ts +8 -1
- package/dist/config.js +9 -1
- package/dist/elicitation.js +5 -1
- package/dist/finance/cost-taxonomy.d.ts +34 -0
- package/dist/finance/cost-taxonomy.js +26 -0
- package/dist/hooks/hook-runner.js +32 -0
- package/dist/http/routes/diagnostics.d.ts +84 -0
- package/dist/http/routes/diagnostics.js +140 -0
- package/dist/http/routes/tasks.js +31 -10
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +6 -0
- package/dist/main.js +34 -3
- package/dist/observability/fail-open.d.ts +98 -0
- package/dist/observability/fail-open.js +216 -0
- package/dist/observability/prompt-manifest.d.ts +13 -0
- package/dist/observability/prompt-manifest.js +8 -0
- package/dist/plugins/approval-store-sql.d.ts +116 -0
- package/dist/plugins/approval-store-sql.js +151 -0
- package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
- package/dist/plugins/file-workflow-journal-store.js +12 -0
- package/dist/plugins/pg-approval-store.d.ts +9 -0
- package/dist/plugins/pg-approval-store.js +9 -0
- package/dist/plugins/pg-breaker-state.d.ts +8 -0
- package/dist/plugins/pg-breaker-state.js +8 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
- package/dist/plugins/pg-checkpoint-store.js +10 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/pg-file-snapshot-store.js +8 -0
- package/dist/plugins/pg-image-bake.d.ts +12 -0
- package/dist/plugins/pg-image-bake.js +11 -0
- package/dist/plugins/pg-image-index.d.ts +12 -0
- package/dist/plugins/pg-image-index.js +11 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
- package/dist/plugins/pg-outcome-ledger.js +11 -0
- package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/pg-resume-anchor-store.js +7 -0
- package/dist/plugins/pg-run-store.d.ts +9 -0
- package/dist/plugins/pg-run-store.js +9 -0
- package/dist/plugins/pg-session-policy-store.d.ts +7 -0
- package/dist/plugins/pg-session-policy-store.js +7 -0
- package/dist/plugins/pg-session-store.d.ts +12 -0
- package/dist/plugins/pg-session-store.js +12 -0
- package/dist/plugins/pg-tool-result-store.d.ts +9 -0
- package/dist/plugins/pg-tool-result-store.js +9 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-journal-store.js +9 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-run-store.js +9 -0
- package/dist/plugins/tidb-approval-store.d.ts +8 -0
- package/dist/plugins/tidb-approval-store.js +8 -0
- package/dist/plugins/tidb-breaker-state.d.ts +7 -0
- package/dist/plugins/tidb-breaker-state.js +7 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
- package/dist/plugins/tidb-checkpoint-store.js +9 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/tidb-file-snapshot-store.js +8 -0
- package/dist/plugins/tidb-image-bake.d.ts +12 -0
- package/dist/plugins/tidb-image-bake.js +11 -0
- package/dist/plugins/tidb-image-index.d.ts +12 -0
- package/dist/plugins/tidb-image-index.js +11 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
- package/dist/plugins/tidb-outcome-ledger.js +12 -0
- package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/tidb-resume-anchor-store.js +7 -0
- package/dist/plugins/tidb-run-store.d.ts +10 -0
- package/dist/plugins/tidb-run-store.js +9 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
- package/dist/plugins/tidb-session-policy-store.js +7 -0
- package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
- package/dist/plugins/tidb-tool-result-store.js +10 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/tidb-workflow-journal-store.js +9 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
- package/dist/plugins/tidb-workflow-run-store.js +10 -0
- package/dist/plugins/workflow-journal-limits.d.ts +12 -0
- package/dist/plugins/workflow-journal-limits.js +12 -0
- package/dist/question.d.ts +18 -14
- package/dist/question.js +83 -34
- package/dist/sema-registry.d.ts +41 -0
- package/dist/sema-registry.js +40 -0
- package/dist/task-settings.d.ts +13 -12
- package/dist/task-settings.js +6 -6
- package/dist/tool-approval.js +15 -7
- package/dist/trace/project.d.ts +36 -1
- package/dist/trace/project.js +55 -2
- package/package.json +3 -3
package/dist/question.d.ts
CHANGED
|
@@ -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
|
|
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 →
|
|
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 ⇒
|
|
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 ⇒
|
|
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
|
|
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
|
|
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 →
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
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
|
|
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)
|
|
19
|
-
*
|
|
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 ⇒
|
|
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
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
const
|
|
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
|
|
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
|
|
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 →
|
|
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(
|
|
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
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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) ⇒
|
|
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
|
|
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
|
|
122
|
-
// THROTTLE (service's job — core imposes none): bound concurrency + total. Breach ⇒
|
|
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
|
|
138
|
+
return UNAVAILABLE;
|
|
126
139
|
const questions = boundQuestions(req.questions);
|
|
127
140
|
if (!questions)
|
|
128
|
-
return
|
|
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(
|
|
151
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -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
|
|
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)
|
|
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).
|
|
169
|
-
* (`src/boot/
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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
|
|
174
|
-
* `
|
|
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
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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;
|
package/dist/task-settings.js
CHANGED
|
@@ -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
|
|
399
|
-
// supply wiring too — same factory, a
|
|
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 →
|
|
411
|
-
// no auto-allow domain at all. Both are the intended fail-safe, NOT a degraded
|
|
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 } : {}),
|
package/dist/tool-approval.js
CHANGED
|
@@ -156,10 +156,18 @@ function boundArgs(args) {
|
|
|
156
156
|
}
|
|
157
157
|
/** #151 车3 刀 3b:`AskRequest.boundInputHash` 的**边界窄读**(车5 §9 C2 的对账 join 键)。
|
|
158
158
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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 键。**窄读**(
|
|
776
|
-
//
|
|
777
|
-
// (`unmatchableNoHash`
|
|
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,
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 五个载体共用一个投影点)。
|