@sema-agent/server 7.99.0 → 7.101.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 (174) hide show
  1. package/MIGRATION.md +35 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +150 -56
  5. package/dist/approval-ask-machine.d.ts +10 -0
  6. package/dist/approval-ask-machine.js +3 -0
  7. package/dist/approval-card.d.ts +86 -1
  8. package/dist/approval-card.js +42 -8
  9. package/dist/approval-policy-settlement.d.ts +31 -0
  10. package/dist/approval-policy-settlement.js +16 -0
  11. package/dist/approval-reconciler.d.ts +8 -1
  12. package/dist/approval-reconciler.js +5 -4
  13. package/dist/approval.d.ts +44 -20
  14. package/dist/approval.js +14 -6
  15. package/dist/boot/config-center.d.ts +16 -8
  16. package/dist/boot/config-center.js +114 -90
  17. package/dist/boot/execution-env.d.ts +1 -2
  18. package/dist/boot/execution-env.js +21 -3
  19. package/dist/boot/leader.js +12 -9
  20. package/dist/boot/memory-consolidation.d.ts +38 -0
  21. package/dist/boot/memory-consolidation.js +35 -0
  22. package/dist/boot/parked-revive-gate.js +1 -0
  23. package/dist/boot/reapers.js +1 -0
  24. package/dist/boot/resolve-spec.d.ts +0 -1
  25. package/dist/boot/resolve-spec.js +28 -10
  26. package/dist/boot/runner-deps.d.ts +2 -2
  27. package/dist/boot/runner-deps.js +9 -3
  28. package/dist/boot/stage-02-registry-pull.js +9 -0
  29. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +3 -3
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  33. package/dist/boot/stage-07-capability-layer.js +6 -6
  34. package/dist/boot/stage-08-reapers.d.ts +3 -4
  35. package/dist/boot/stage-08-reapers.js +2 -2
  36. package/dist/boot/stage-09-leader.d.ts +2 -3
  37. package/dist/boot/stage-10-http-server.d.ts +2 -3
  38. package/dist/boot/stage-10-http-server.js +2 -2
  39. package/dist/boot/stores.js +4 -0
  40. package/dist/capabilities/center-plugins.d.ts +14 -2
  41. package/dist/capabilities/center-plugins.js +13 -5
  42. package/dist/capabilities/hands-lane.js +2 -1
  43. package/dist/capabilities/scenarios.js +18 -5
  44. package/dist/capabilities/tool-defer.d.ts +2 -2
  45. package/dist/config-catalog.d.ts +2 -2
  46. package/dist/config-catalog.js +16 -15
  47. package/dist/config-center/apply-effective.d.ts +59 -39
  48. package/dist/config-center/apply-effective.js +231 -120
  49. package/dist/config-center/apply-ledger.d.ts +45 -16
  50. package/dist/config-center/apply-ledger.js +28 -18
  51. package/dist/config-center/facade.d.ts +12 -6
  52. package/dist/config-center/facade.js +2 -2
  53. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  54. package/dist/config-center/hot-keys-registry.js +11 -7
  55. package/dist/config-center/http-client.d.ts +4 -1
  56. package/dist/config-center/http-client.js +15 -10
  57. package/dist/config-center/read-warnings.d.ts +115 -6
  58. package/dist/config-center/read-warnings.js +115 -5
  59. package/dist/config-center/restart-signal.d.ts +21 -14
  60. package/dist/config-center/restart-signal.js +16 -22
  61. package/dist/config-center/types.d.ts +9 -3
  62. package/dist/config-invariants.d.ts +2 -2
  63. package/dist/config-invariants.js +15 -0
  64. package/dist/config-provider.d.ts +4 -0
  65. package/dist/config-provider.js +64 -9
  66. package/dist/config-types.d.ts +57 -13
  67. package/dist/config.js +87 -22
  68. package/dist/cross-session-settings.js +1 -1
  69. package/dist/deployment-governance.d.ts +45 -12
  70. package/dist/deployment-governance.js +37 -9
  71. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  72. package/dist/env-name-allowlist-knobs.js +24 -0
  73. package/dist/fleet-client.d.ts +2 -1
  74. package/dist/hooks/hook-llm.js +13 -4
  75. package/dist/host-lsp-manager.d.ts +41 -0
  76. package/dist/host-lsp-manager.js +15 -0
  77. package/dist/http/active-run-conflict.d.ts +2 -12
  78. package/dist/http/active-run-conflict.js +1 -6
  79. package/dist/http/admission.js +30 -7
  80. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  81. package/dist/http/memory-store-refusal-reply.js +10 -0
  82. package/dist/http/resume-legs.d.ts +1 -1
  83. package/dist/http/resume-legs.js +14 -10
  84. package/dist/http/route-ctx.d.ts +2 -2
  85. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  86. package/dist/http/routes/approvals-assistant.js +3 -2
  87. package/dist/http/routes/capabilities.js +3 -3
  88. package/dist/http/routes/memory-bundle.js +3 -0
  89. package/dist/http/routes/memory-compliance.js +3 -0
  90. package/dist/http/routes/memory-origin.js +3 -0
  91. package/dist/http/routes/sessions.js +7 -0
  92. package/dist/http/routes/side-query.js +10 -4
  93. package/dist/http/routes/tasks.js +7 -6
  94. package/dist/http/server.d.ts +6 -5
  95. package/dist/http/wire-types.d.ts +6 -3
  96. package/dist/instruction-source-notice.d.ts +97 -0
  97. package/dist/instruction-source-notice.js +78 -0
  98. package/dist/leader/wire.d.ts +20 -12
  99. package/dist/leader/wire.js +7 -6
  100. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  101. package/dist/memory-layer-legacy-lock.js +74 -0
  102. package/dist/memory-store-refusal.d.ts +37 -0
  103. package/dist/memory-store-refusal.js +14 -0
  104. package/dist/model-entry-refusal.d.ts +130 -0
  105. package/dist/model-entry-refusal.js +157 -0
  106. package/dist/model-provider.d.ts +9 -3
  107. package/dist/model-route-endpoint.d.ts +1 -1
  108. package/dist/model-select.d.ts +49 -4
  109. package/dist/model-select.js +48 -17
  110. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  111. package/dist/observability/corrupt-read-seat.js +31 -0
  112. package/dist/observability/fail-open.d.ts +16 -4
  113. package/dist/observability/fail-open.js +16 -4
  114. package/dist/observability/metrics.js +1 -0
  115. package/dist/observability/run-terminal.js +1 -0
  116. package/dist/observability/secret-env-scrub.d.ts +27 -3
  117. package/dist/observability/secret-env-scrub.js +22 -6
  118. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  119. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  120. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  121. package/dist/plugins/approval-ask-store-file.js +21 -3
  122. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  123. package/dist/plugins/approval-ask-store-memory.js +39 -13
  124. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  125. package/dist/plugins/approval-ask-store-sql.js +27 -3
  126. package/dist/plugins/caching-session-store.d.ts +6 -0
  127. package/dist/plugins/caching-session-store.js +6 -0
  128. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  129. package/dist/plugins/checkpoint-store-sql.js +19 -2
  130. package/dist/plugins/local-checkpoint-store.js +2 -0
  131. package/dist/plugins/local-session-store.d.ts +20 -25
  132. package/dist/plugins/local-session-store.js +111 -64
  133. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  134. package/dist/plugins/mailbox-store-sql.js +6 -4
  135. package/dist/plugins/permission-rule-store-file.js +3 -2
  136. package/dist/plugins/pg-session-storage.d.ts +7 -0
  137. package/dist/plugins/pg-session-storage.js +19 -0
  138. package/dist/plugins/remote-env-host.d.ts +3 -1
  139. package/dist/plugins/remote-env-host.js +3 -6
  140. package/dist/plugins/sql-json-column.d.ts +16 -3
  141. package/dist/plugins/sql-json-column.js +15 -4
  142. package/dist/plugins/tidb-pool.js +5 -0
  143. package/dist/plugins/tidb-session-store.d.ts +13 -0
  144. package/dist/plugins/tidb-session-store.js +14 -0
  145. package/dist/plugins/web-search.d.ts +44 -23
  146. package/dist/plugins/web-search.js +161 -50
  147. package/dist/project-memory.d.ts +10 -0
  148. package/dist/project-memory.js +28 -14
  149. package/dist/run-local.d.ts +17 -15
  150. package/dist/run-local.js +119 -18
  151. package/dist/runs.js +6 -5
  152. package/dist/runtime-governance.d.ts +50 -3
  153. package/dist/runtime-governance.js +5 -0
  154. package/dist/sealed-key.d.ts +5 -2
  155. package/dist/server-secret-env.d.ts +26 -2
  156. package/dist/server-secret-env.js +9 -0
  157. package/dist/task-settings.d.ts +1 -1
  158. package/dist/tool-approval.d.ts +150 -28
  159. package/dist/tool-approval.js +150 -54
  160. package/dist/trace/core-keyset-guard.d.ts +30 -8
  161. package/dist/trace/engine-notice-wire.d.ts +27 -1
  162. package/dist/trace/engine-notice-wire.js +8 -0
  163. package/dist/trace/ledger-sink.js +2 -2
  164. package/dist/trace/project.d.ts +13 -0
  165. package/dist/trace/project.js +22 -5
  166. package/dist/trace/projection-drop.d.ts +2 -0
  167. package/dist/trace/projection-drop.js +6 -0
  168. package/dist/trace/sema-provenance.d.ts +4 -1
  169. package/dist/trace/sema-provenance.js +1 -1
  170. package/dist/trace/task-notification-facets.d.ts +65 -0
  171. package/dist/trace/task-notification-facets.js +31 -0
  172. package/dist/trace/wire-projection-faces.d.ts +3 -3
  173. package/dist/trace/wire-projection-faces.js +3 -3
  174. package/package.json +3 -3
package/dist/run-local.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_MODEL_REF } from "./model-select.js";
2
+ import { DEFAULT_MODEL_REF, resolveTaskModel } from "./model-select.js";
3
+ import { defaultModelRefusal, modelUnavailableMessage } from "./model-entry-refusal.js";
3
4
  import { buildDurableAbsenceNote, createCorruptReadSeat } from "./observability/corrupt-read-seat.js";
4
5
  import { outwardPlane } from "./terminal.js";
5
6
  import { homedir } from "node:os";
@@ -8,8 +9,8 @@ import { realpathSync } from "node:fs";
8
9
  import { createInterface } from "node:readline/promises";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { loadRemoteExec } from "@sema-agent/settings-schema/node";
11
- import { Runner, uuidv7, parseModelMention, FileStorageBackend, NodeExecutionEnv, NodeLspManager, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
12
- import { hasOperatorGateIntent } from "./approval.js";
12
+ import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
13
+ import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
13
14
  import { createBrain } from "./brain.js";
14
15
  import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
15
16
  import { applyRuntimeGovernance } from "./runtime-governance.js";
@@ -25,6 +26,7 @@ import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario,
25
26
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
26
27
  import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
27
28
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
29
+ import { createInstructionSourceSeat } from "./instruction-source-notice.js";
28
30
  import { loadDeploymentSkills } from "./capabilities/skills.js";
29
31
  import { createRepoClient } from "./capabilities/repo-tools.js";
30
32
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
@@ -48,6 +50,7 @@ import { fatalExit } from "./boot/fatal-exit.js";
48
50
  import { installProcessExitBoundary } from "./boot/exit-boundary.js";
49
51
  import { createMetrics } from "./observability/metrics.js";
50
52
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
53
+ import { createHostLspManager } from "./host-lsp-manager.js";
51
54
  import { buildDisplaySafeUrl } from "./trace/redact.js";
52
55
  export function parseArgs(argv) {
53
56
  let objective;
@@ -121,22 +124,81 @@ export function missingGatewayRequirements(config) {
121
124
  }
122
125
  return missing;
123
126
  }
127
+ const RUN_LOCAL_APPROVAL_SEAT = { kind: "live" };
124
128
  export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot) {
125
- return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
129
+ return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config, RUN_LOCAL_APPROVAL_SEAT) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
126
130
  }
127
131
  export function createLocalApprover(io) {
132
+ let tail = Promise.resolve();
128
133
  return async (req, signal) => {
129
134
  if (!io.isTty) {
130
- io.printErr(`denied "${req.toolName}" (${req.message}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
131
- `The gate comes from this deployment's governance knobs: AUTONOMY, MANUAL_MODE_SHELL_GATE, ` +
132
- `runtime.commandPolicy (config.d/governance.json). Run it in a terminal to answer interactively, or relax the knob.`);
135
+ io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
136
+ `The gate comes from this deployment's governance knobs: APPROVAL_REQUIRE / APPROVAL_NEVER_AUTO (the approval lists; ` +
137
+ `APPROVAL_DENY refuses outright without asking), AUTONOMY, MANUAL_MODE_SHELL_GATE, runtime.commandPolicy (config.d/governance.json). ` +
138
+ `Run it in a terminal to answer interactively, or relax the knob (APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls; ` +
139
+ `a separate .env for this CLI keeps a server's lists off it).`);
133
140
  return false;
134
141
  }
135
- return io.askYesNo(`approve "${req.toolName}"? ${req.message} [y/N] `, signal);
142
+ const argsShown = argsDisplayOf(req.args);
143
+ if (!argsShown.ok) {
144
+ io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — its arguments cannot be displayed ` +
145
+ `(${terminalSafeText(argsShown.reason)}), so there is nothing a person could review; refusing instead of asking.`);
146
+ return false;
147
+ }
148
+ const prior = tail;
149
+ let release;
150
+ const mine = new Promise((resolve) => {
151
+ release = resolve;
152
+ });
153
+ tail = prior.then(() => mine);
154
+ try {
155
+ if (!(await priorSettledUnlessAborted(prior, signal)))
156
+ return false;
157
+ return await io.askYesNo(buildLocalAskPrompt(req, argsShown.text), signal);
158
+ }
159
+ finally {
160
+ release();
161
+ }
136
162
  };
137
163
  }
138
- export async function promptYesNoOnTty(prompt, signal, streams = { input: process.stdin, output: process.stderr }) {
164
+ function priorSettledUnlessAborted(prior, signal) {
139
165
  if (signal?.aborted)
166
+ return Promise.resolve(false);
167
+ if (signal === undefined)
168
+ return prior.then(() => true);
169
+ return new Promise((resolve) => {
170
+ const onAbort = () => resolve(false);
171
+ signal.addEventListener("abort", onAbort, { once: true });
172
+ void prior.then(() => {
173
+ signal.removeEventListener("abort", onAbort);
174
+ resolve(!signal.aborted);
175
+ });
176
+ });
177
+ }
178
+ function argsDisplayOf(args) {
179
+ let json;
180
+ try {
181
+ json = JSON.stringify(args);
182
+ }
183
+ catch (e) {
184
+ return { ok: false, reason: e instanceof Error ? e.message : String(e) };
185
+ }
186
+ if (json === undefined)
187
+ return { ok: false, reason: "the arguments serialize to nothing" };
188
+ return { ok: true, text: terminalSafeText(json) };
189
+ }
190
+ function buildLocalAskPrompt(req, argsShown) {
191
+ return `approve "${terminalSafeText(req.toolName)}"? ${terminalSafeText(req.message)}\n ${argsShown}\n[y/N] `;
192
+ }
193
+ function terminalSafeText(text) {
194
+ return text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
195
+ }
196
+ function inputAlreadyEnded(input) {
197
+ const state = input;
198
+ return state.readableEnded === true || state.destroyed === true;
199
+ }
200
+ export async function promptYesNoOnTty(prompt, signal, streams = { input: process.stdin, output: process.stderr }) {
201
+ if (signal?.aborted || inputAlreadyEnded(streams.input))
140
202
  return false;
141
203
  const rl = createInterface({ input: streams.input, output: streams.output });
142
204
  try {
@@ -157,6 +219,27 @@ export async function promptYesNoOnTty(prompt, signal, streams = { input: proces
157
219
  rl.close();
158
220
  }
159
221
  }
222
+ function personStopOf(result) {
223
+ if (result.terminal.kind !== "completed")
224
+ return undefined;
225
+ if (result.haltedOnUserRejection === true)
226
+ return "haltedOnUserRejection";
227
+ if (result.haltedByUser === true)
228
+ return "haltedByUser";
229
+ return undefined;
230
+ }
231
+ function buildPersonStopFooter(stop, sessionId) {
232
+ switch (stop) {
233
+ case "haltedOnUserRejection":
234
+ return `task stopped (haltedOnUserRejection): a tool call was refused, so the run did not finish its work — continue it with --session ${sessionId}`;
235
+ case "haltedByUser":
236
+ return `task stopped (haltedByUser): the run was halted before it finished its work — continue it with --session ${sessionId}`;
237
+ default: {
238
+ const unhandled = stop;
239
+ throw new Error(`person-stop marker "${String(unhandled)}" has no footer`);
240
+ }
241
+ }
242
+ }
160
243
  export async function runLocal(argv, deps = {}) {
161
244
  const print = deps.print ?? ((l) => console.log(l));
162
245
  const printErr = deps.printErr ?? ((l) => console.error(l));
@@ -194,8 +277,10 @@ export async function runLocal(argv, deps = {}) {
194
277
  if (r) {
195
278
  for (const de of r.domainErrors ?? [])
196
279
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "domain fell to schema defaults; other domains applied normally" });
280
+ for (const d of r.droppedEntries ?? [])
281
+ logger.warn("config_entry_dropped", { domain: d.domain, entry: d.entry, error: d.error, version: r.effective.version, phase: "boot", note: "this one entry failed its domain schema and was dropped on its own — the rest of the candidate applies (per-entry tolerance); fix the named entry to bring it back" });
197
282
  effective = r.effective;
198
- applyEffective(config, r.effective, logger);
283
+ applyEffective(config, r.effective, logger, { leg: "boot" });
199
284
  if (effective?.mcp)
200
285
  config.mcpServers = resolveMcpServers(effective.mcp, logger);
201
286
  if (effective?.a2a)
@@ -207,6 +292,9 @@ export async function runLocal(argv, deps = {}) {
207
292
  catch (err) {
208
293
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
209
294
  }
295
+ const noDefaultModel = defaultModelRefusal(config, "env");
296
+ if (noDefaultModel !== undefined)
297
+ throw new Error(noDefaultModel);
210
298
  const endpointRefusal = modelRouteEndpointRefusal(config);
211
299
  if (endpointRefusal !== undefined)
212
300
  throw new Error(endpointRefusal);
@@ -241,8 +329,8 @@ export async function runLocal(argv, deps = {}) {
241
329
  return 2;
242
330
  }
243
331
  const ungatedCompensation = buildOnlySensitiveBaselineWarning(config, {
244
- durableEnabled: false,
245
- singleUserAutoAcceptBaseline: config.requirePrincipal !== true && !hasOperatorGateIntent(config),
332
+ approvalSeat: RUN_LOCAL_APPROVAL_SEAT.kind,
333
+ singleUserAutoAcceptBaseline: singleUserAutoAcceptBaselineApplies(config),
246
334
  });
247
335
  if (ungatedCompensation)
248
336
  logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
@@ -285,7 +373,7 @@ export async function runLocal(argv, deps = {}) {
285
373
  ...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
286
374
  })
287
375
  : undefined;
288
- const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) }) : undefined;
376
+ const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? createHostLspManager(config, (event, fields) => logger.info(event, fields)) : undefined;
289
377
  const memoryEngine = memoryEngineBackendFor(config, root, memoryEngineIncident);
290
378
  let memorySync;
291
379
  if (config.memorySync && memoryEngine) {
@@ -416,7 +504,10 @@ export async function runLocal(argv, deps = {}) {
416
504
  onError: engineOnError,
417
505
  onAsk,
418
506
  ...(config.projectMemoryEnabled
419
- ? { loadProjectMemory: makeLoadProjectMemory({ logger }), probeInstructionSources: makeProbeInstructionSources() }
507
+ ? {
508
+ loadProjectMemory: makeLoadProjectMemory({ logger, instructionSource: createInstructionSourceSeat(sessionStore, sharedRunnerDeps.onNotice) }),
509
+ probeInstructionSources: makeProbeInstructionSources(),
510
+ }
420
511
  : {}),
421
512
  };
422
513
  const runner = new Runner(runnerDeps);
@@ -459,7 +550,14 @@ export async function runLocal(argv, deps = {}) {
459
550
  }
460
551
  throw err;
461
552
  }
462
- const mention = parseModelMention(args.objective, Object.keys(config.models));
553
+ const mention = resolveTaskModel(undefined, args.objective, config.models, undefined, config.modelRefusals);
554
+ if (mention.unavailable !== undefined) {
555
+ const { requested, name, refusal } = mention.unavailable;
556
+ printErr(`cannot run: ${modelUnavailableMessage("@-mention", requested, name, refusal)}`);
557
+ void lspManager?.dispose();
558
+ await fileBackend.dispose();
559
+ return 2;
560
+ }
463
561
  const scope = args.user ?? "local";
464
562
  const taskTimeoutSec = Math.floor(parseNumOrFailNonNegative("TASK_TIMEOUT_SEC", process.env.TASK_TIMEOUT_SEC || "0"));
465
563
  const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "discuss");
@@ -473,13 +571,13 @@ export async function runLocal(argv, deps = {}) {
473
571
  ...(maxTokens !== undefined ? { maxTokens } : {}),
474
572
  };
475
573
  const spec = applyLocalGovernance({
476
- objective: mention.cleanedText || args.objective,
574
+ objective: mention.cleanedObjective,
477
575
  sessionId,
478
576
  backgroundScope: "session",
479
577
  oneShot: true,
480
578
  principal: scope,
481
579
  ...(memoryEngine ? { memory: memorySpecForRequest(scope, memoryProjectKeyFor(config, scope, workspaceDir), undefined) } : {}),
482
- model: mention.model ?? "default",
580
+ model: mention.model,
483
581
  getApiKeyAndHeaders: keyResolver,
484
582
  tools: cap.tools,
485
583
  skills: cap.skills,
@@ -526,11 +624,14 @@ export async function runLocal(argv, deps = {}) {
526
624
  await fileBackend.dispose();
527
625
  return 1;
528
626
  }
627
+ const personStop = personStopOf(result);
529
628
  if (args.json) {
530
629
  print(JSON.stringify(result, null, 2));
531
630
  }
532
631
  else if (result.terminal.kind === "completed") {
533
632
  print(result.result);
633
+ if (personStop !== undefined)
634
+ printErr(buildPersonStopFooter(personStop, sessionId));
534
635
  }
535
636
  else {
536
637
  const plane = outwardPlane(result.terminal);
@@ -539,7 +640,7 @@ export async function runLocal(argv, deps = {}) {
539
640
  print(salvage);
540
641
  printErr(`task ${plane.status}${plane.errorCode ? ` (${plane.errorCode})` : ""}${plane.errorMessage ? `: ${plane.errorMessage}` : ""}`);
541
642
  }
542
- if (result.terminal.kind !== "completed")
643
+ if (result.terminal.kind !== "completed" || personStop !== undefined)
543
644
  exitCode = 1;
544
645
  if (memorySync) {
545
646
  try {
package/dist/runs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { runWithVerification, runCascade, defaultTaskRegistry, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
1
+ import { runWithVerification, runCascade, defaultTaskRegistry, taskNotificationDedupKey, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
2
  import { failureCodeOf, sealResultForExit, outwardPlane } from "./terminal.js";
3
3
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
4
  import { redactErrorMessage } from "./observability/run-terminal.js";
@@ -12,7 +12,7 @@ import { CANCELLED_CODE, mintCancelledResult } from "./run-cancel-context.js";
12
12
  import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
13
13
  import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
14
14
  import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
15
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
15
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
16
16
  import { recordFailOpen } from "./observability/fail-open.js";
17
17
  import { errorText as errText } from "./observability/err-text.js";
18
18
  import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
@@ -256,8 +256,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
256
256
  const { type, ...rest } = frame;
257
257
  const f = frame;
258
258
  const write = append(type, rest);
259
- if (f.type === "task_notification" && f.task_id)
260
- notifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
259
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
260
+ if (fKey !== undefined)
261
+ notifiedKeys.set(fKey, write.then(() => true, () => false));
261
262
  return write;
262
263
  }, completionDiagLog ? { route: "bg-run-start", connection: "durable-append", log: completionDiagLog } : undefined);
263
264
  const runBody = async () => {
@@ -285,7 +286,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
285
286
  void workflowCompletionInbox.enqueue(taskNotificationInboxEntry(spec.sessionId, elicitOwner ?? owner, n, Date.now(), taskId, opts?.priority)).catch((err) => completionDiagLog?.("park_enqueue_failed", { route: "bg-run-start", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
286
287
  }
287
288
  else if (legLive) {
288
- const key = taskNotificationStreamKey(n);
289
+ const key = taskNotificationDedupKey(n);
289
290
  if (notifiedKeys.get(key) === undefined) {
290
291
  notifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
291
292
  }
@@ -23,7 +23,7 @@
23
23
  * path-prefixed commands all bypass the argv[0] match (they route to `ask`, fail-closed, but are not blocked by
24
24
  * name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
25
25
  */
26
- import { type TaskSpec, type ToolPolicy } from "@sema-agent/core";
26
+ import { type ShellGateDoctrine, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
27
27
  import { GovernanceConfig } from "@sema-agent/settings-schema";
28
28
  import { type GovernanceAskMarks } from "./governance-ask-marks.js";
29
29
  /** 运维 autonomy 四档的**闭集**(同上)。`config.ts` 的 `parseAutonomy` 也从这里读 —— 四档词表全仓一份。 */
@@ -142,13 +142,60 @@ export declare function autonomyOverrides(autonomy: Autonomy | undefined): Parti
142
142
  * 并不要求 always),之后运维把 autonomy 热改成 `ask`,那条老行会被标成治理出身 —— 五个前提要**同时**
143
143
  * 成立,且该行仍未决。
144
144
  * 根治要**在 mint 那一刻把出身写进行里**(core 的 `CheckpointGate` 属主面,不是本仓能单方面做的);
145
- * 在那之前这一位的定位是**分诊提示**,不是裁决输入 —— 它不参与任何门/CAS/resume 判定,消费端也只拿它
146
- * 渲染徽标。登记在此,不加特征化测试(把已知残留钉成契约是另一种病)。
145
+ * 在那之前这一位的定位是**分诊提示**,不参与任何门/CAS/resume 判定 —— 唯一的例外是**只收窄**的那一个读者:
146
+ * 耐久 `/decide` 腿的会话级 remember 可记性(NP-1,7.100.0,`tool-approval.ts` 的 `parkRowStandingAnswerFacts`)。
147
+ * 在那里两个失真方向的代价都不越过修前:在场向 ⇒ 一次 `remember:"session"` 只放行本次、不铸会话免询问(安全
148
+ * 方向,代价 = 下一次再问一遍);缺席向 ⇒ 照旧可记(= NP-1 之前的行为)。它从不**放宽**任何裁决。
149
+ * 登记在此,不加特征化测试(把已知残留钉成契约是另一种病)。
147
150
  */
148
151
  export declare function governanceMandatesShellGateAlways(governance: {
149
152
  autonomy?: Autonomy;
150
153
  manualModeShellGate?: "always" | "classify";
151
154
  }): boolean;
155
+ /** [ref] 归因的部署侧入参(两个治理旋钮;判据属主见 {@link governanceMandatesShellGateAlways})。 */
156
+ export interface GovernancePosture {
157
+ autonomy?: Autonomy;
158
+ manualModeShellGate?: "always" | "classify";
159
+ }
160
+ /**
161
+ * [ref]:推「这道门是运维治理层下的吗」。判据是**合取**,两半各管一件事:
162
+ *
163
+ * 行上的取证格 `gate.riskDescriptor.shellGateDoctrine === "always"`
164
+ * ∧ 本部署的治理层本来就要求 always({@link governanceMandatesShellGateAlways},那里写着为什么单看行不够 ——
165
+ * SUP 路由姿态同样产 `"always"`)
166
+ *
167
+ * 第一半:core 在 mint 这道门时把「当时活着的 doctrine」如实写进 `riskDescriptor.shellGateDoctrine`
168
+ * (2026-08-05 取证裁定;`prepare-task` 只在**被 shell 门铸**的 ask 上写它),所以持久行自己带着证据 ——
169
+ * park 腿天然跨副本、跨重启,活卡腿那张 ALS 标记表在这里结构上够不着。
170
+ * 第二半:把「有效档 always」收窄到「治理层是真成因」,否则一条 SUP 路由出来的门会被谎报成治理强制。
171
+ *
172
+ * ⚠️ 残留(双向失真的窄在场向 + 根治路径)与「只收窄」的那一个裁决读者,都登记在
173
+ * {@link governanceMandatesShellGateAlways} 的顶注,别在这里复述。
174
+ *
175
+ * 🔴 `"classify"` 档**刻意不标**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能出自分类器(治理)
176
+ * 也可能出自 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
177
+ *
178
+ * 🔴 **单一属主,四个读者**(不在任何读面另写一份判据 —— 这一位恰恰是靠「行上取证 ∧ 部署姿态」的合取才不
179
+ * 谎报的,两份必漂):409 体的待批门材料(`http/active-run-conflict.ts`)、两条 durable 读面
180
+ * `GET /v1/approvals` / `/v1/approvals/stream` 的 `projectPendingForWire`([ref]②)、耐久 `/decide` 腿的
181
+ * remember 可记性(NP-1,经 `tool-approval.ts` 的 `parkRowStandingAnswerFacts`)。
182
+ * 住在本文件而不是 `http/active-run-conflict.ts` 的理由:它的另一半判据属主就在这里,而第四个读者
183
+ * (`tool-approval.ts`)从 `http/` 那侧值引会闭合一条 ESM 值环(`active-run-conflict` → `checkpoint-store-sql` →
184
+ * `tool-approval`),本文件是三者共同的叶。
185
+ *
186
+ * ⚠️ **askId 为什么不在这里**([ref] 的另一半,如实记账):待批 ask 的 `askId` 只活在 `approval_ask` 行上
187
+ * (checkpoint 行没有这一列,core 的 `CheckpointGate`/`PendingAction`/`Checkpoint` 顶层都不带它)。反向
188
+ * (checkpoint → ask)今天**没有读口**:两条 list 读口都硬过滤 `state='STREAM_PENDING'`,而 park 完的行是
189
+ * `PARKED`;唯一精确的连接键是 ask 行上的 `gate_token`,要按它反查得给店加一个新读口(SQL 面 = 双库集成
190
+ * 门)。派生 `deriveAskId(...)` 也不行:它吃 `legKey`(= 上一腿 resume token 的摘要)与 `parentToolCallId`,
191
+ * 这两维从 checkpoint 行推不出来,二腿/子代形上会算出一个**错**的 id —— 在「等的谁」这条通道上,错 id
192
+ * 比缺席坏得多(`http/active-run-conflict.ts` 的 `resumeEntryForGate` 的 default 臂是同一条纪律)。故诚实缺席。
193
+ */
194
+ export declare function governanceOriginOf(gate: {
195
+ riskDescriptor?: {
196
+ shellGateDoctrine?: ShellGateDoctrine;
197
+ };
198
+ } | undefined, governance: GovernancePosture | undefined): true | undefined;
152
199
  /** spec 序里的秩(0 起,越大越严)。入参收 `string`(fail-loud 边界:schema `trustRank` 同款姿势)——词表外 ⇒
153
200
  * RangeError,绝不 undefined/NaN 让比较静默成 false。导出只为对表钉(test/safety-merge-spec-consumption.test.ts,
154
201
  * [ref] 件4)——运行时消费仍走上表(键类型闭集)。 */
@@ -116,6 +116,11 @@ export function autonomyOverrides(autonomy) {
116
116
  export function governanceMandatesShellGateAlways(governance) {
117
117
  return autonomyOverrides(governance.autonomy).shellGate === "always" || governance.manualModeShellGate === "always";
118
118
  }
119
+ export function governanceOriginOf(gate, governance) {
120
+ if (gate?.riskDescriptor?.shellGateDoctrine !== "always")
121
+ return undefined;
122
+ return governanceMandatesShellGateAlways(governance ?? {}) ? true : undefined;
123
+ }
119
124
  function createGovernanceAskMarkingPolicy(inner, marksOf) {
120
125
  const note = (d, req) => {
121
126
  marksOf()?.setMarked(req.toolCallId, d.action === "ask");
@@ -23,8 +23,11 @@ export interface SealedKeyStore extends SealedKeyOpener {
23
23
  createdAt: string;
24
24
  };
25
25
  }
26
- /** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). */
27
- export type SealedKeyPoisonReason = "sealed_decrypt_failed" | "stale_ciphertext" | "unsupported_alg" | "no_key_store";
26
+ /** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). CUSTODY failures only:
27
+ * a sealedApiKey that is malformed (alg other than the pinned v1 SEALED_KEY_ALG, non-base64 ciphertext) makes the whole
28
+ * catalog entry unavailable instead (S-704 `config.model_entry_invalid`, settings-schema `SealedApiKey` is the shape
29
+ * owner) — the former `unsupported_alg` reason was retired with that move (it could no longer be reached). */
30
+ export type SealedKeyPoisonReason = "sealed_decrypt_failed" | "stale_ciphertext" | "no_key_store";
28
31
  /**
29
32
  * Poison-pill entry for the per-model key map (`config.modelApiKeys`): planted by applyEffective when
30
33
  * a model CARRIES `sealedApiKey` but the ciphertext cannot be unsealed. We deliberately do NOT drop
@@ -11,12 +11,28 @@
11
11
  * ## 规则(一句)
12
12
  * 本仓**知道**哪些 env 键是凭据 —— 配置目录(`config-catalog.ts`)里 `type:"secret"` 的每一行,从目录机器派生,
13
13
  * 不手抄第二份名单 —— 这些键按名剥;其余照旧交 core 按形剥。{@link scrubServerSecrets} = 目录派生剥除 ∘ core
14
- * `scrubSecretEnv`,两条腿都只调它。core 形状规则的缺口由 core 源头修(另案);本层不改 core 的判据,也不自造
15
- * 第二份形状规则。目录里加一个 secret 型键,两条腿当场就剥它。
14
+ * `scrubSecretEnv`,本仓三条子进程腿(host shell / git 子进程 / 本地语言服务器,后者见 `host-lsp-manager.ts`)都只调它。
15
+ * core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072 已补 `KEYS|TOKENS|PASSWORDS`(S-616 提货),目录臂独担的只剩
16
+ * `OTEL_EXPORTER_OTLP_HEADERS`(`*_HEADERS` 不在形状规则射程,是边界不是缺陷票)与三只名单型旋钮 `LSP_ENV_ALLOW`(S-699)/
17
+ * `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(S-713):原文可能夹着一枚被错展开进来的令牌,按凭据类登记 —— 形状规则按名判,本来就不该
18
+ * 认它们)。本层不改 core 的判据,也不自造
19
+ * 第二份形状规则。目录里加一个 secret 型键,三条腿当场就剥它。
16
20
  *
17
21
  * ## 不变的面
18
22
  * `inheritEnv:"all"`(显式全继承)与显式 allowlist 两形不经本函数,行为逐字不变;调用方显式传入的 env
19
23
  * (`cfg.env` / 单次命令的 `env`)照旧原样覆盖在剥除结果之上。
24
+ *
25
+ * ## 点名放行(S-699 / S-713,一只谓词三处用)
26
+ * {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给子进程」的唯一实现。三处调用:
27
+ * host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts`)、本地语言服务器的 `LSP_ENV_ALLOW`(`host-lsp-manager.ts`)、
28
+ * 沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(`config.ts`,S-713 起;此前那里是第三份自写的循环)。三只名单型
29
+ * 旋钮的**名单**另由 `config.ts` 的同一只解析器 `parseEnvNameAllowlist` 判形(形错拒启 / 源里没有的名按序号 warn),本函数只管取值。
30
+ * 「点名」在三处同义:点到的键原样带值(空串值照带),密钥形也照过(点名 = 部署方显式承担);源里没有的名不产生条目。
31
+ * 沙箱两腿不继承本进程环境:进沙箱的本进程值只有点到的键(与 host shell 的 hermetic 形同一侧)。
32
+ * host shell 与语言服务器**只在没点到的键上**不同,这是有意保留的差别:host shell 的 `string[]` 形是 hermetic 白名单(与 core
33
+ * `NodeExecutionEnv` 同名选项同义,没点到的一律不继承);语言服务器在 {@link scrubServerSecrets} 的结果之上放回点名键
34
+ * (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic 形会让语言服务器当场找不到自己的工具链)。
35
+ * 两处都不放宽对方:把 host shell 改成「剥后放回」= 在模型可驱动的 shell 上放宽,把语言服务器改成 hermetic = 功能当场失灵。
20
36
  */
21
37
  import { type SecretEnvFinding } from "@sema-agent/core";
22
38
  /** 目录派生臂的一条剥除记录(形同 core 的 `SecretEnvFinding`:键名 / 规则 / 置信度 / 来源;值永不携带)。
@@ -38,4 +54,12 @@ export declare const CATALOG_SECRET_ENV_NAMES: ReadonlySet<string>;
38
54
  * 目录臂的计数恰好就是 core 形状规则的缺口读数。
39
55
  */
40
56
  export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
57
+ /**
58
+ * S-699 —— **点名放行**的唯一谓词:从 `env` 里把 `names` 点到的键**原样**取出(值逐字,空串照带;源里没有的名不产生
59
+ * 条目)。密钥形的键点到了也照取 —— 点名即部署方显式承担;要不要在这之上再叠别的基底,是调用方的事(见文件头「点名放行」节)。
60
+ * 纯函数,不留痕:点名放回的键不是剥除,不进 `secret_env_scrubbed` 的键表与计数。
61
+ * 返回形 `Record<string, string>`(S-713):只装源里在的键,值恒是字符串 —— 沙箱两腿(`sandboxEnv` / docker `env`)要的正是这个形,
62
+ * 不必在调用点再过滤一遍 `undefined`。
63
+ */
64
+ export declare function allowlistedEnv(names: readonly string[], env: NodeJS.ProcessEnv): Record<string, string>;
41
65
  //# sourceMappingURL=server-secret-env.d.ts.map
@@ -15,4 +15,13 @@ export function scrubServerSecrets(env, findings) {
15
15
  }
16
16
  return out;
17
17
  }
18
+ export function allowlistedEnv(names, env) {
19
+ const out = {};
20
+ for (const name of names) {
21
+ const value = env[name];
22
+ if (value !== undefined)
23
+ out[name] = value;
24
+ }
25
+ return out;
26
+ }
18
27
  //# sourceMappingURL=server-secret-env.js.map
@@ -264,7 +264,7 @@ export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768;
264
264
  export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
265
265
  /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
266
266
  * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在
267
- * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
267
+ * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(判决 `judgeRequestWebSearch`,单用户闸;形错在受理面 400,S-199)。
268
268
  * 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。
269
269
  *
270
270
  * 🔴 两个 `skillListing*`([ref],7.77.0)= **CC settings 同名键**,值域抄 CC 的 schema 声明