@sema-agent/server 7.92.1 → 7.93.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 (60) hide show
  1. package/README.md +1 -0
  2. package/README.zh-CN.md +1 -0
  3. package/dist/approval-ask-machine.d.ts +1 -1
  4. package/dist/approval-card.d.ts +10 -7
  5. package/dist/approval-card.js +3 -3
  6. package/dist/bench/s1/arms.js +5 -6
  7. package/dist/bench/s1/live-deps.js +2 -2
  8. package/dist/boot/resolve-spec.js +1 -0
  9. package/dist/boot/stage-06-runners.js +3 -2
  10. package/dist/boot/stores.js +9 -0
  11. package/dist/capabilities/mcp-probe.d.ts +290 -0
  12. package/dist/capabilities/mcp-probe.js +318 -0
  13. package/dist/http/resume-legs.js +12 -10
  14. package/dist/http/route-ctx.d.ts +3 -0
  15. package/dist/http/routes/capabilities.d.ts +17 -0
  16. package/dist/http/routes/capabilities.js +93 -1
  17. package/dist/http/routes/memory-origin.d.ts +1 -1
  18. package/dist/http/routes/rules.js +49 -13
  19. package/dist/http/routes/runs.js +3 -2
  20. package/dist/http/routes/session-sync.js +24 -12
  21. package/dist/http/routes/sessions.js +12 -8
  22. package/dist/http/routes/tasks.js +13 -13
  23. package/dist/http/send.d.ts +11 -0
  24. package/dist/http/send.js +9 -0
  25. package/dist/http/server.js +3 -2
  26. package/dist/http/wire-types.d.ts +17 -1
  27. package/dist/leader/fanout.js +3 -3
  28. package/dist/leader/leader.js +4 -4
  29. package/dist/leader/repair-wire.js +2 -2
  30. package/dist/leader/wire.js +2 -2
  31. package/dist/observability/metrics.js +2 -0
  32. package/dist/permission-rule-vocab.d.ts +9 -9
  33. package/dist/plugins/file-run-store.js +3 -3
  34. package/dist/plugins/memory-run-store.js +3 -3
  35. package/dist/plugins/remote-env-host.d.ts +5 -0
  36. package/dist/plugins/remote-env-host.js +39 -37
  37. package/dist/plugins/run-store-sql.js +3 -3
  38. package/dist/plugins/schema-consistency.d.ts +88 -0
  39. package/dist/plugins/schema-consistency.js +202 -0
  40. package/dist/plugins/schema-ensure-legs.d.ts +55 -0
  41. package/dist/plugins/schema-ensure-legs.js +87 -0
  42. package/dist/plugins/session-policy-store-sql.js +23 -5
  43. package/dist/request-key-closure.js +1 -1
  44. package/dist/rules-consent.d.ts +42 -4
  45. package/dist/rules-consent.js +23 -7
  46. package/dist/run-local.js +2 -2
  47. package/dist/runs.js +14 -14
  48. package/dist/task-mcp.d.ts +32 -0
  49. package/dist/task-mcp.js +7 -0
  50. package/dist/terminal.d.ts +58 -4
  51. package/dist/terminal.js +28 -6
  52. package/dist/tool-approval.d.ts +22 -1
  53. package/dist/tool-approval.js +1 -0
  54. package/dist/trace/core-keyset-guard.d.ts +3 -3
  55. package/dist/trace/ledger-sink.js +3 -2
  56. package/dist/trace/project.d.ts +24 -0
  57. package/dist/trace/project.js +40 -37
  58. package/dist/trace/wire-projection-faces.d.ts +209 -0
  59. package/dist/trace/wire-projection-faces.js +62 -0
  60. package/package.json +3 -3
@@ -3,7 +3,7 @@ import { sendJson, sendError, httpErrorCode } from "../send.js";
3
3
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import { createLogger } from "../../observability/logger.js";
6
- import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, sameWireRuleIdentity, serializeRuleScope } from "../../rules-consent.js";
6
+ import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
7
7
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
8
8
  import { HttpError } from "../../security.js";
9
9
  import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
@@ -246,6 +246,40 @@ async function handleRuleList(res, search, caller, lane, operatorPrincipals) {
246
246
  : undefined;
247
247
  sendJson(res, 200, { rules: page, rev: listing.rev, ...(nextCursor !== undefined ? { nextCursor } : {}) });
248
248
  }
249
+ function sendRuleWriteFailure(res, verb, committed) {
250
+ switch (verb) {
251
+ case "write":
252
+ switch (committed) {
253
+ case "no":
254
+ sendError(res, 503, "state.rule_write_failed", "this rule was not written and nothing landed in the store — send the same request again", { committed });
255
+ return;
256
+ case "unknown":
257
+ sendError(res, 503, "state.rule_write_failed", "this write may or may not have landed — reconcile with GET /v1/rules before sending it again", { committed });
258
+ return;
259
+ default: {
260
+ const unknownWord = committed;
261
+ throw new Error(`unknown rule-write committed word: ${JSON.stringify(unknownWord)}`);
262
+ }
263
+ }
264
+ case "revoke":
265
+ switch (committed) {
266
+ case "no":
267
+ sendError(res, 503, "state.rule_remove_failed", "this rule was not revoked and nothing landed in the store — send the same request again", { committed });
268
+ return;
269
+ case "unknown":
270
+ sendError(res, 503, "state.rule_remove_failed", "this revocation may or may not have landed — reconcile with GET /v1/rules before sending it again", { committed });
271
+ return;
272
+ default: {
273
+ const unknownWord = committed;
274
+ throw new Error(`unknown rule-remove committed word: ${JSON.stringify(unknownWord)}`);
275
+ }
276
+ }
277
+ default: {
278
+ const unknownVerb = verb;
279
+ throw new Error(`unknown rule write verb: ${JSON.stringify(unknownVerb)}`);
280
+ }
281
+ }
282
+ }
249
283
  async function handleRuleWrite(res, rawBody, caller, lane, operatorPrincipals) {
250
284
  const parsed = RuleIdentityBodySchema.safeParse(rawBody);
251
285
  if (!parsed.success) {
@@ -271,7 +305,9 @@ async function handleRuleWrite(res, rawBody, caller, lane, operatorPrincipals) {
271
305
  switch (written.status) {
272
306
  case "persisted":
273
307
  case "no-op":
274
- sendJson(res, 200, { status: written.status, rev: written.rev, rule: written.row });
308
+ sendJson(res, 200, written.row === undefined
309
+ ? { status: written.status, rev: written.rev, stillLive: written.stillLive }
310
+ : { status: written.status, rev: written.rev, stillLive: written.stillLive, rule: written.row });
275
311
  return;
276
312
  case "rejected":
277
313
  logger.info("rule write refused", { reason: written.reason, detail: written.detail });
@@ -280,9 +316,9 @@ async function handleRuleWrite(res, rawBody, caller, lane, operatorPrincipals) {
280
316
  detail: written.detail,
281
317
  });
282
318
  return;
283
- case "indeterminate":
284
- logger.info("rule write indeterminate", { detail: written.detail });
285
- sendError(res, 503, "state.rule_write_failed", "this write could not be confirmed as a standing rule — retry, then reconcile with GET /v1/rules");
319
+ case "failed":
320
+ logger.info("rule write failed", { detail: written.detail, committed: written.committed });
321
+ sendRuleWriteFailure(res, "write", written.committed);
286
322
  return;
287
323
  default: {
288
324
  const unknown = written;
@@ -311,17 +347,17 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
311
347
  case "removed":
312
348
  sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
313
349
  return;
314
- case "no-op": {
315
- const stillLive = (await lane.listRules(target.principal)).rows.some((r) => sameWireRuleIdentity(r, { behavior: parsed.data.behavior, rule: parsed.data.rule, scope: serializeRuleScope(scope) }))
316
- ? "yes"
317
- : "no";
318
- sendJson(res, 200, { status: "no-op", rev: outcome.rev, stillLive });
350
+ case "no-op":
351
+ sendJson(res, 200, { status: "no-op", rev: outcome.rev, stillLive: outcome.stillLive });
319
352
  return;
320
- }
321
353
  case "failed":
322
- logger.info("rule revoke failed", { error: outcome.error });
323
- sendError(res, 503, "state.rule_remove_failed", "the rule could not be revoked right now — retry, then reconcile with GET /v1/rules");
354
+ logger.info("rule revoke failed", { error: outcome.error, committed: outcome.committed });
355
+ sendRuleWriteFailure(res, "revoke", outcome.committed);
324
356
  return;
357
+ default: {
358
+ const unknown = outcome;
359
+ throw new Error(`unknown rule revoke outcome: ${JSON.stringify(unknown)}`);
360
+ }
325
361
  }
326
362
  }
327
363
  export const RULES_ROUTES = [
@@ -2,7 +2,7 @@ import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { isModelTextLedgerEventType } from "../../trace/ledger-events.js";
4
4
  import { z } from "zod";
5
- import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
5
+ import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
6
6
  import { isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
7
7
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
8
8
  import { redactSecrets } from "../../trace/redact.js";
@@ -1058,8 +1058,9 @@ export async function handleRunVerbs(req, res, match, ctx) {
1058
1058
  const g14 = (() => {
1059
1059
  const d = out.details;
1060
1060
  const replay = d?.details?.replay;
1061
- if (replay === "cursor" || replay === "full")
1061
+ if (typeof replay === "string" && TASK_OUTPUT_REPLAY_KINDS.includes(replay)) {
1062
1062
  return { cursorSemantics: replay };
1063
+ }
1063
1064
  return {};
1064
1065
  })();
1065
1066
  sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details, ...g14 });
@@ -1,11 +1,11 @@
1
1
  import { once } from "node:events";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
- import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, SessionPolicyError, validateFileHistoryExport } from "@sema-agent/core";
3
+ import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, validateFileHistoryExport } from "@sema-agent/core";
4
4
  import { StringDecoder } from "node:string_decoder";
5
5
  import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, overwriteWipeIncapable, wipeSessionAttendants } from "../../session-sync.js";
6
6
  import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
7
7
  import { isUuidShape } from "../../security.js";
8
- import { sendJson, sendError, msg } from "../send.js";
8
+ import { sendJson, sendError, sendSessionPolicyError, msg } from "../send.js";
9
9
  import { buildDisplaySafeUrl } from "../../trace/redact.js";
10
10
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
11
11
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
@@ -13,7 +13,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
13
13
  const SYNC_IMPORT_BATCH = 500;
14
14
  import { SHA256_HEX_RE } from "../../digest-form.js";
15
15
  import { assertNever } from "../route-ctx.js";
16
- import { readSessionRulesRecords, describeSessionRulesRecordProblem, sessionPolicyErrorHttp } from "../../session-policy-wire.js";
16
+ import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
17
17
  export function createSessionSyncLocal() {
18
18
  const pendingStagings = new Map();
19
19
  const importLeases = new Map();
@@ -74,7 +74,15 @@ export async function handleSessionSync(req, res, match, ctx) {
74
74
  if (match.decl.id === "session-sync-manifest") {
75
75
  if (!pullGateOk())
76
76
  return;
77
- const out = await exportSessionManifest(sessionId, deps.backend);
77
+ let out;
78
+ try {
79
+ out = await exportSessionManifest(sessionId, deps.backend);
80
+ }
81
+ catch (e) {
82
+ if (sendSessionPolicyError(res, e))
83
+ return;
84
+ throw e;
85
+ }
78
86
  if (out === null) {
79
87
  sendError(res, 404, "not_found.session", "session not found");
80
88
  return;
@@ -281,8 +289,16 @@ export async function handleSessionSync(req, res, match, ctx) {
281
289
  }
282
290
  }
283
291
  const opOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
284
- if (deps.sessionPolicyStore)
285
- await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, opOk);
292
+ if (deps.sessionPolicyStore) {
293
+ try {
294
+ await replayPolicyRecords(deps.sessionPolicyStore, sessionId, policy, opOk);
295
+ }
296
+ catch (e) {
297
+ if (sendSessionPolicyError(res, e))
298
+ return;
299
+ throw e;
300
+ }
301
+ }
286
302
  const importingP = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
287
303
  if (deps.resumeAnchorStore)
288
304
  for (const a of anchors)
@@ -499,13 +515,9 @@ export async function handleSessionSync(req, res, match, ctx) {
499
515
  sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
500
516
  return;
501
517
  }
502
- if (e instanceof SessionPolicyError) {
503
- cleanupStaging(stagingId);
504
- const h = sessionPolicyErrorHttp(e.code);
505
- sendError(res, h.status, h.errorCode, e.message);
506
- return;
507
- }
508
518
  cleanupStaging(stagingId);
519
+ if (sendSessionPolicyError(res, e))
520
+ return;
509
521
  throw e;
510
522
  }
511
523
  return;
@@ -1,12 +1,12 @@
1
- import { materializeA2aTools, materializeMcpTools, sanitizePathComponent, SessionPolicyError } from "@sema-agent/core";
1
+ import { materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
2
2
  import { a2aForScenario, mcpForScenario } from "../../config-center/facade.js";
3
3
  import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
4
4
  import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
8
- import { readSessionPermissionRules, describeSessionPermissionRulesProblem, sessionPolicyErrorHttp } from "../../session-policy-wire.js";
9
- import { sendJson, sendError, sseHeaders } from "../send.js";
8
+ import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
9
+ import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
10
10
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
11
11
  import { assertNever } from "../route-ctx.js";
12
12
  const MCP_STATUS_TTL_MS = 5_000;
@@ -731,7 +731,14 @@ export async function handleSessions(req, res, match, ctx) {
731
731
  }
732
732
  const target = owner;
733
733
  if (match.decl.id === "session-policy-get") {
734
- sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
734
+ try {
735
+ sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
736
+ }
737
+ catch (e) {
738
+ if (sendSessionPolicyError(res, e))
739
+ return;
740
+ throw e;
741
+ }
735
742
  return;
736
743
  }
737
744
  let body;
@@ -767,11 +774,8 @@ export async function handleSessions(req, res, match, ctx) {
767
774
  sendJson(res, 200, { rules: stored });
768
775
  }
769
776
  catch (e) {
770
- if (e instanceof SessionPolicyError) {
771
- const h = sessionPolicyErrorHttp(e.code);
772
- sendError(res, h.status, h.errorCode, e.message);
777
+ if (sendSessionPolicyError(res, e))
773
778
  return;
774
- }
775
779
  throw e;
776
780
  }
777
781
  return;
@@ -1,7 +1,7 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
- import { runWithVerification, runCascade, uuidv7, terminalProjection } from "@sema-agent/core";
2
+ import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
3
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
- import { stripResumeToken } from "../../terminal.js";
4
+ import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
@@ -47,7 +47,7 @@ function sendSubmitResult(res, resp) {
47
47
  : raw;
48
48
  sendJson(res, 200, body);
49
49
  }
50
- const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For observable events submit via POST /v1/runs and read GET /v1/runs/:id/events.";
50
+ const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For status / progress / delegated-child frames and the terminal done, submit via POST /v1/runs and read GET /v1/runs/:id/events; note that verify / cascade legs carry no per-turn stream there either (no wiring_manifest / text / turn_end frames — the engine runs those legs as result-returning attempts).";
51
51
  export async function handleTasks(req, res, match, ctx) {
52
52
  const { deps } = ctx;
53
53
  const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
@@ -58,7 +58,7 @@ export async function handleTasks(req, res, match, ctx) {
58
58
  const noteRunOutcome = (r) => {
59
59
  if (!r?.terminal)
60
60
  return;
61
- const plane = terminalProjection(r.terminal);
61
+ const plane = outwardPlane(r.terminal);
62
62
  reqState.runStatus = plane.status;
63
63
  if (plane.errorCode)
64
64
  reqState.runErrorCode = plane.errorCode;
@@ -421,7 +421,7 @@ export async function handleTasks(req, res, match, ctx) {
421
421
  userMsgEntryId = ev.entryId;
422
422
  const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
423
423
  if (ev.type === "done") {
424
- finalResult = redactTerminalResult(stripResumeToken(await graftParkToolCallId(ev.result, deps.checkpointStore)));
424
+ finalResult = redactTerminalResult(sealResultForExit(await graftParkToolCallId(ev.result, deps.checkpointStore)));
425
425
  if (durableTaskId && finalResult)
426
426
  finalResult = { ...finalResult, taskId: durableTaskId };
427
427
  if (cancelVerbAtDone && finalResult !== undefined) {
@@ -430,7 +430,7 @@ export async function handleTasks(req, res, match, ctx) {
430
430
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
431
431
  noteRunOutcome(finalResult);
432
432
  putRewindAnchor();
433
- settleFleet(terminalProjection(finalResult.terminal).status, fleetRunResiduals(finalResult));
433
+ settleFleet(outwardPlane(finalResult.terminal).status, fleetRunResiduals(finalResult));
434
434
  if (ledgerSink && finalResult) {
435
435
  if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
436
436
  const rs2 = deps.runStore;
@@ -650,7 +650,7 @@ export async function handleTasks(req, res, match, ctx) {
650
650
  }, withApproval)
651
651
  : withApproval());
652
652
  if (durableTaskId && deps.runStore) {
653
- const finalPlane = finalResult ? terminalProjection(finalResult.terminal) : undefined;
653
+ const finalPlane = finalResult ? outwardPlane(finalResult.terminal) : undefined;
654
654
  if (finalPlane?.status === "suspended" && deps.checkpointStore)
655
655
  await deps.runStore.setSuspended(durableTaskId);
656
656
  else if (finalPlane?.status === "needs_review" && deps.checkpointStore)
@@ -717,7 +717,7 @@ export async function handleTasks(req, res, match, ctx) {
717
717
  ledgerTerminal = true;
718
718
  await ledgerSink.onDone(finalResult).catch(() => undefined);
719
719
  }
720
- const retryPlane = terminalProjection(finalResult.terminal);
720
+ const retryPlane = outwardPlane(finalResult.terminal);
721
721
  await deps.runStore.setTerminal(durableTaskId, retryPlane.status, finalResult, retryPlane.errorMessage ?? null);
722
722
  settleFleet(retryPlane.status, fleetRunResiduals(finalResult));
723
723
  }
@@ -860,15 +860,15 @@ export async function handleTasks(req, res, match, ctx) {
860
860
  }
861
861
  const resumeAtStatus = resumeAtHttpStatus(result);
862
862
  if (resumeAtStatus) {
863
- const resumeAtPlane = terminalProjection(result.terminal);
863
+ const resumeAtPlane = outwardPlane(result.terminal);
864
864
  if (durableTaskId && deps.runStore)
865
- await deps.runStore.setTerminal(durableTaskId, "failed", stripResumeToken(result), resumeAtPlane.errorMessage ?? null);
865
+ await deps.runStore.setTerminal(durableTaskId, "failed", sealResultForExit(result), resumeAtPlane.errorMessage ?? null);
866
866
  const retryAfterSec = resumeAtPlane.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
867
867
  ? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
868
868
  : undefined;
869
869
  return { status: resumeAtStatus, body: { errorCode: resumeAtPlane.errorCode, error: redactErrorMessage(resumeAtPlane.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
870
870
  }
871
- const syncPlane = terminalProjection(result.terminal);
871
+ const syncPlane = outwardPlane(result.terminal);
872
872
  if (syncPlane.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
873
873
  if (durableTaskId)
874
874
  await deps.runStore.setSuspended(durableTaskId);
@@ -880,10 +880,10 @@ export async function handleTasks(req, res, match, ctx) {
880
880
  return { status: 200, body: withSyncSubmitKeys({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
881
881
  }
882
882
  if (durableTaskId && deps.runStore)
883
- await deps.runStore.setTerminal(durableTaskId, syncPlane.status, stripResumeToken(result), syncPlane.errorMessage ?? null);
883
+ await deps.runStore.setTerminal(durableTaskId, syncPlane.status, sealResultForExit(result), syncPlane.errorMessage ?? null);
884
884
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
885
885
  noteRunOutcome(result);
886
- return { status: 200, body: withSyncSubmitKeys(redactTerminalResult(stripResumeToken(result))) };
886
+ return { status: 200, body: withSyncSubmitKeys(redactTerminalResult(sealResultForExit(result))) };
887
887
  };
888
888
  const resp = await idemCache.run(idemKey, async () => withRunReceipt(await runSyncSubmitLeg()), (r) => r.status < 400);
889
889
  sendSubmitResult(res, resp);
@@ -60,5 +60,16 @@ export declare function sendError(res: ServerResponse, status: number, errorCode
60
60
  * throw 点里只有 3 个带。为了让「errorCode 恒在」在这些站点也成立(而不是靠豁免表放行),按**状态码**
61
61
  * 给一个诚实的粗码:它只声明「这类拒绝」,不冒充比 throw 点更细的语义。带 `code` 的 throw 点原样优先。 */
62
62
  export declare function httpErrorCode(status: number, code?: string): string;
63
+ /**
64
+ * 🔴 B1 / S-535 —— `SessionPolicyError` → 应答,经 {@link sessionPolicyErrorHttp} 那**一只**映射
65
+ * (`src/session-policy-wire.ts`)。回 `true` = 已应答,调用点 `return`。
66
+ *
67
+ * 为什么是一只共享腿而不是每个站点一句 `instanceof`:映射本身早就收成一处了,而**咨询它的站点**只有两条
68
+ * **写**腿 —— 偏偏 core 7.25.0 那个新词 `corrupt` 是在**读**上抛出来的(店读不出它自己写的那一行:
69
+ * S-529 的两道读回拒 + C1 的体形拒)。于是 `GET /v1/sessions/:id/policy` 与 2c 的 manifest 导出腿把它交给了
70
+ * 域外通用臂 ⇒ `500 internal.error "internal error"`:码丢,连带**修复路径**也丢(同一行 `PUT` 却答
71
+ * `500 corrupt` 并点名那一列)。一只腿之后,新站点只会忘记调它,不会再各自发明一份映射。
72
+ */
73
+ export declare function sendSessionPolicyError(res: ServerResponse, e: unknown): boolean;
63
74
  export declare function msg(e: unknown): string;
64
75
  //# sourceMappingURL=send.d.ts.map
package/dist/http/send.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { SessionPolicyError } from "@sema-agent/core";
2
+ import { sessionPolicyErrorHttp } from "../session-policy-wire.js";
1
3
  export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
2
4
  export function isSseResponse(res) {
3
5
  return res[SSE_RESPONSE_MARK] === true;
@@ -35,6 +37,13 @@ export function httpErrorCode(status, code) {
35
37
  default: return status >= 500 ? "internal.error" : "request.rejected";
36
38
  }
37
39
  }
40
+ export function sendSessionPolicyError(res, e) {
41
+ if (!(e instanceof SessionPolicyError))
42
+ return false;
43
+ const h = sessionPolicyErrorHttp(e.code);
44
+ sendError(res, h.status, h.errorCode, e.message);
45
+ return true;
46
+ }
38
47
  export function msg(e) {
39
48
  return e instanceof Error ? e.message : String(e);
40
49
  }
@@ -1,6 +1,7 @@
1
1
  import http from "node:http";
2
+ import { outwardPlane } from "../terminal.js";
2
3
  import { redactHead } from "../observability/run-terminal.js";
3
- import { validatePendingSteer, terminalProjection } from "@sema-agent/core";
4
+ import { validatePendingSteer } from "@sema-agent/core";
4
5
  import {} from "../config-center/facade.js";
5
6
  import { HttpError } from "../security.js";
6
7
  import {} from "../memory-sync.js";
@@ -117,7 +118,7 @@ export function createHttpServer(rawDeps) {
117
118
  return deps.runnerFor?.(spec) ?? deps.runner;
118
119
  }
119
120
  function recordTaskResult(result) {
120
- const plane = terminalProjection(result.terminal);
121
+ const plane = outwardPlane(result.terminal);
121
122
  deps.metrics?.inc("tasks_total", { status: plane.status });
122
123
  if (result.stats.tokens)
123
124
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
@@ -6,7 +6,7 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, McpServerSpec, SkillSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApproverPosture, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
10
  import type { SettingsPermissionMode } from "../task-settings.js";
11
11
  import type { wiringManifestFaceEventData } from "../trace/project.js";
12
12
  export interface TaskRequestBody {
@@ -316,6 +316,22 @@ export interface TaskRequestBody {
316
316
  * `SETTINGS_PERMISSION_MODES`(受理腿 `isSettingsPermissionMode` 与两句 400 文案同读那一份)。
317
317
  * 改前这里是第 6 份手抄的五词联合 —— 加/退一个模式时 wire 型面与受理腿会各走各的。 */
318
318
  permissionMode?: SettingsPermissionMode;
319
+ /**
320
+ * **#1000**(core 7.26.0 `TaskSpec.approverPosture`;CC-116 装配口)—— 宿主对这个 run 的**审批姿态声明**:
321
+ * 「这些卡由我自己自动答」。一个词(`"bypass"`),闭集属主在 core(`ApproverPosture` / `APPROVER_POSTURES`)。
322
+ *
323
+ * 🔴 **它不授权任何东西,也不是一只门**(core 头注逐字:"a statement about who answers this run's approval
324
+ * cards, never an instruction to the gate";引擎只把它**渲**给模型,没有任何裁决腿读它)。所以它与
325
+ * {@link permissionMode} 的 `"bypassPermissions"` **不是**一回事,也不能互相派生:后者选的是**门的形状**
326
+ * (且只能收紧、deny-wins),前者只说「谁来答卡」。两者在语义上同族,声明它而不开 `bypassPermissions`
327
+ * 只是对模型说了一句与实际不符的话 —— 见契约文档那一段的「谁受伤」。
328
+ *
329
+ * 🔴 **本仓零判据、原样过境**(`boot/resolve-spec.ts` 写点):词的拒绝逻辑属引擎
330
+ * (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server **不加第二道门** —— 在下游再判一次就是
331
+ * 同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加第二个词就会被静默 400 掉。
332
+ * 型也从 core 的 `ApproverPosture` 派生(不抄字面联合)。
333
+ */
334
+ approverPosture?: ApproverPosture;
319
335
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
320
336
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the [ref] memory ENGINE materializes/
321
337
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
@@ -1,6 +1,6 @@
1
- import { terminalProjection } from "@sema-agent/core";
1
+ import {} from "@sema-agent/core";
2
2
  import { redactErrorMessage, redactWorkerReportError } from "../observability/run-terminal.js";
3
- import { pausedCauseOf } from "../terminal.js";
3
+ import { pausedCauseOf, outwardPlane } from "../terminal.js";
4
4
  class Semaphore {
5
5
  max;
6
6
  active = 0;
@@ -68,7 +68,7 @@ export async function fanOut(subtasks, opts) {
68
68
  let report;
69
69
  try {
70
70
  const result = await stream.result();
71
- const plane = terminalProjection(result.terminal);
71
+ const plane = outwardPlane(result.terminal);
72
72
  report = { ...base, status: mapStatus(result.terminal), stats: result.stats, ...(plane.errorMessage ? { error: redactErrorMessage(plane.errorMessage) ?? "" } : {}) };
73
73
  }
74
74
  catch (e) {
@@ -2,8 +2,8 @@ import { fanOut } from "./fanout.js";
2
2
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
3
3
  import { pullDiff } from "./diffout.js";
4
4
  import { mergeBranches } from "./merge.js";
5
- import { Runner, runWithVerification, resumeWithVerification, terminalProjection } from "@sema-agent/core";
6
- import { pausedCauseOf } from "../terminal.js";
5
+ import { Runner, runWithVerification, resumeWithVerification } from "@sema-agent/core";
6
+ import { pausedCauseOf, outwardPlane } from "../terminal.js";
7
7
  const MAX_RESUME_SLICES = 200;
8
8
  export async function autoResumeOnResource(initial, signal, resume, maxSlices = MAX_RESUME_SLICES) {
9
9
  let vr = initial;
@@ -59,7 +59,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
59
59
  deps.logger?.info?.("worker_resource_resume", { workerId: w.workerId, slice });
60
60
  return runner.resume(token, { gate: "resource_limit", decision: "continue" }, { ...w.spec, sessionId: w.sessionId, signal });
61
61
  });
62
- const plane = terminalProjection(r.terminal);
62
+ const plane = outwardPlane(r.terminal);
63
63
  const paused = pausedCauseOf(r.terminal);
64
64
  if (r.terminal.kind !== "completed") {
65
65
  deps.logger?.warn?.("worker_failed", {
@@ -92,7 +92,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
92
92
  deps.logger?.info?.("worker_resource_resume", { workerId: w.workerId, slice });
93
93
  return resumeWithVerification(w.runner, token, { gate: "resource_limit", decision: "continue" }, { ...w.spec, sessionId: w.sessionId, signal }, w.spec.objective ?? "", cfg);
94
94
  });
95
- const vrPlane = terminalProjection(vr.terminal);
95
+ const vrPlane = outwardPlane(vr.terminal);
96
96
  deps.logger?.info?.("verify_outcome", { workerId: w.workerId, status: vrPlane.status, verdict: vr.verification.verdict, rounds: vr.verification.rounds, finding: redactHead(vr.verification.findings[0] ?? "", 220) });
97
97
  const pass = vr.terminal.kind === "completed" && vr.verification.verdict === "PASS";
98
98
  const vrPaused = pausedCauseOf(vr.terminal);
@@ -1,6 +1,6 @@
1
1
  import { runRepairLoop, delimitUntrusted, } from "@sema-agent/core";
2
2
  import { mkRepairOracle } from "./repair-oracle.js";
3
- import { terminalProjection } from "@sema-agent/core";
3
+ import { outwardPlane } from "../terminal.js";
4
4
  import { redactHead } from "../observability/run-terminal.js";
5
5
  function statusForTerminal(t) {
6
6
  return t === "fixed" || t === "candidate_only" ? "completed" : "failed";
@@ -52,7 +52,7 @@ async function runRepairLoopForLeaderInner(solo, implSpec, cfg) {
52
52
  return { report: { ...base, status: "failed", error: safe }, result: undefined };
53
53
  }
54
54
  const terminal = result.repairTerminal;
55
- const plane = terminalProjection(result.terminal);
55
+ const plane = outwardPlane(result.terminal);
56
56
  const report = {
57
57
  ...base,
58
58
  status: terminal === undefined ? "failed" : statusForTerminal(terminal),
@@ -1,4 +1,4 @@
1
- import { terminalProjection } from "@sema-agent/core";
1
+ import { outwardPlane } from "../terminal.js";
2
2
  import { parseNumOrFail } from "../config.js";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
@@ -176,7 +176,7 @@ export function createLeaderRunner(cfg) {
176
176
  })
177
177
  .result();
178
178
  if (res.terminal.kind !== "completed")
179
- throw new Error(`repair round ${round} did not complete (status=${terminalProjection(res.terminal).status})`);
179
+ throw new Error(`repair round ${round} did not complete (status=${outwardPlane(res.terminal).status})`);
180
180
  return { costUsd: res.stats?.costMicroUsd !== undefined ? res.stats.costMicroUsd / 1e6 : undefined, note: res.result?.slice(0, 200) };
181
181
  }
182
182
  finally {
@@ -1,5 +1,6 @@
1
1
  import { DENIED_BY_VALUES } from "@sema-agent/core";
2
2
  import { buildPermissionRuleEventsHelp } from "./permission-rule-events.js";
3
+ import { MCP_PROBE_OUTCOMES } from "../capabilities/mcp-probe.js";
3
4
  const DEFAULT_BUCKETS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
4
5
  function labelKey(labels) {
5
6
  return Object.keys(labels)
@@ -214,6 +215,7 @@ export function createMetrics() {
214
215
  m.counter("http_requests_total", "HTTP requests by route/method/status");
215
216
  m.histogram("http_request_duration_seconds", "HTTP request duration");
216
217
  m.counter("rate_limited_total", "Requests rejected by the rate limiter");
218
+ m.counter("mcp_probe_total", `No-run MCP status face outcomes by outcome (${MCP_PROBE_OUTCOMES.join("/")} — see src/capabilities/mcp-probe.ts MCP_PROBE_OUTCOMES; rate_limited has TWO causes: the per-identity fixed window AND the replica-wide in-flight cap, which can refuse caller B because of caller A)`);
217
219
  m.counter("tasks_total", "Finished tasks by terminal status");
218
220
  m.counter("task_tokens_total", "Total model tokens consumed by tasks");
219
221
  m.histogram("task_duration_seconds", "Async run wall-clock duration");
@@ -1,4 +1,4 @@
1
- import type { RemoveResult, RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
1
+ import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
2
2
  /**
3
3
  * `PersistedRuleTool` **不再是闭词表**(core 7.9.0 / [ref]):工具名自 core 的**目录**派生
4
4
  * (`pathTarget` 声明 ⇒ 路径文法;`Bash` ⇒ 命令文法),型是 `string`。于是本文件对它的处置从
@@ -61,13 +61,13 @@ export declare const RULE_APPROVAL_RECORD_SCHEMA: RuleApprovalRecord["schema"];
61
61
  * `"unknown"` = **回答这一问的那次读回没做成** —— 这个域唯一不肯说的那句话是「看不见 ⇒ 没有」,
62
62
  * 第三个词就是它的反面。
63
63
  *
64
- * 🔴 **型从 core 的导出型派生,不是抄一份字面联合**:`RemovalLiveness` 本身没有从包根导出
65
- * (亲验 `dist/index.d.ts`:只导 `RemoveResult`),而 `RemoveResult` 的 removed 臂上就带着它 ⇒ 从那里
66
- * `Extract` 出来。抄一份 `"yes"|"no"|"unknown"` 的下场与本文件顶注说的那一族一样:core 加第四个词
67
- * (或改掉某一个)时,抄件不会红。派生形则**编译期**先红在每一处穷举上。
68
- * (已向 core 请托把这只型放上包根导出面;到货后本行换成直接 import,词表值零变化。)
64
+ * 🔴 **型从 core 的包根导出接过来,本仓零抄**(core 7.26.0 S-538 ⓐ:`RemovalLiveness` 随请托上了导出面)。
65
+ * 上一版是 `Extract<RemoveResult, {status:"removed"}>["stillLive"]` —— 那是派生而非手抄(值不会漂),但它把
66
+ * 一只**独立的闭集**寄生在某一个臂的形状上:core 哪天给 `removed` 臂改名、或让这只词表先在别的臂上落地,
67
+ * 派生式就会指向一个与词表无关的东西。现在两者同名同源。
68
+ * 🔴 **运行期词表 `REMOVAL_LIVENESS_WORDS` 刻意不 re-export**:本仓对这三个词零运行期判据(值由 core 在两只
69
+ * 200 体上原样透传,分支全走编译期穷尽),开一扇没人走的门只会让台账多一行「引用而不消费」的例外。
70
+ * 哪天真要判「这个串在不在表里」,那时从 core 直接读它。
69
71
  */
70
- export type RemovalLiveness = Extract<RemoveResult, {
71
- status: "removed";
72
- }>["stillLive"];
72
+ export type { RemovalLiveness } from "@sema-agent/core";
73
73
  //# sourceMappingURL=permission-rule-vocab.d.ts.map
@@ -1,9 +1,9 @@
1
1
  import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal.js";
2
+ import { outwardPlane } from "../terminal.js";
2
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
3
4
  import { join } from "node:path";
4
5
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
5
6
  import { randomBytes } from "node:crypto";
6
- import { terminalProjection } from "@sema-agent/core";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { StoreBootRefusalError } from "./store-contracts.js";
@@ -371,7 +371,7 @@ export class FileRunStore {
371
371
  r.status = status;
372
372
  r.result = result;
373
373
  r.error = error;
374
- r.errorCode = result === null ? null : (terminalProjection(result.terminal).errorCode ?? null);
374
+ r.errorCode = result === null ? null : (outwardPlane(result.terminal).errorCode ?? null);
375
375
  r.updatedAt = new Date();
376
376
  this.persist(r);
377
377
  pending = {
@@ -382,7 +382,7 @@ export class FileRunStore {
382
382
  resultTaskId: result?.taskId,
383
383
  model: result?.model,
384
384
  errorCode: r.errorCode ?? undefined,
385
- errorMessage: (result === null ? undefined : terminalProjection(result.terminal).errorMessage) ?? error ?? undefined,
385
+ errorMessage: (result === null ? undefined : outwardPlane(result.terminal).errorMessage) ?? error ?? undefined,
386
386
  elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
387
387
  };
388
388
  }
@@ -1,6 +1,6 @@
1
1
  import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal.js";
2
+ import { outwardPlane } from "../terminal.js";
2
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
3
- import { terminalProjection } from "@sema-agent/core";
4
4
  import { notifyRunTerminal } from "../observability/run-terminal.js";
5
5
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
6
6
  export class MemoryRunStore {
@@ -121,7 +121,7 @@ export class MemoryRunStore {
121
121
  r.status = status;
122
122
  r.result = result;
123
123
  r.error = error;
124
- r.errorCode = result === null ? null : (terminalProjection(result.terminal).errorCode ?? null);
124
+ r.errorCode = result === null ? null : (outwardPlane(result.terminal).errorCode ?? null);
125
125
  r.updatedAt = new Date();
126
126
  pending = {
127
127
  runId: taskId,
@@ -131,7 +131,7 @@ export class MemoryRunStore {
131
131
  resultTaskId: result?.taskId,
132
132
  model: result?.model,
133
133
  errorCode: r.errorCode ?? undefined,
134
- errorMessage: (result === null ? undefined : terminalProjection(result.terminal).errorMessage) ?? error ?? undefined,
134
+ errorMessage: (result === null ? undefined : outwardPlane(result.terminal).errorMessage) ?? error ?? undefined,
135
135
  elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
136
136
  };
137
137
  }
@@ -134,6 +134,11 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
134
134
  * ② 把读拉进来会让 teardown 去等一次可能**永不 settle** 的读(workspace 里的 FIFO / 特殊文件),
135
135
  * 把 destroy 挂死 —— 拿一条新的可用性病换一条不存在的正确性收益。
136
136
  * 判据因此是一句话:**「可能建路径或改路径的那些面」进在飞集合**,不是「过了 fsReady 的都进」。
137
+ *
138
+ * ── 不止文件面(S-541)─────────────────────────────────────────────────────────────────────────
139
+ * 判据是「会不会建/改路径」,**不是**「在不在 `env` 的文件面上」。后台 shell 的 `launch`(`makeBgDriver`)
140
+ * 在 spool 根下 `mkdir` 一棵子树,而它整段期间 `bgManager.hasShells()` 还是 false(`shells.set` 在
141
+ * `await launch(ctx)` 之后)⇒ 同一条竞态的另一张脸,同一只集合治。见那处头注。
137
142
  */
138
143
  private withInflightFs;
139
144
  /**