@sema-agent/server 7.128.0 → 7.129.1

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 (105) hide show
  1. package/MIGRATION.md +140 -0
  2. package/USAGE.md +114 -18
  3. package/dist/approval-card.d.ts +53 -26
  4. package/dist/approval-card.js +40 -11
  5. package/dist/approval-crash-convergence.js +4 -4
  6. package/dist/bake-runner/main.js +2 -1
  7. package/dist/boot/execution-env.js +1 -0
  8. package/dist/boot/memory-consolidation.d.ts +46 -16
  9. package/dist/boot/memory-consolidation.js +15 -8
  10. package/dist/boot/side-query-lane.d.ts +7 -20
  11. package/dist/boot/side-query-lane.js +0 -24
  12. package/dist/boot/stage-01-config.js +3 -0
  13. package/dist/boot/stage-06-runners.js +1 -1
  14. package/dist/boot/stores.js +2 -2
  15. package/dist/boot/web-admin.js +17 -10
  16. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  17. package/dist/boot/webfetch-summarize-lane.js +2 -3
  18. package/dist/brain.js +6 -9
  19. package/dist/capabilities/builtin-tools.d.ts +2 -2
  20. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  21. package/dist/capabilities/engine-capabilities.js +12 -0
  22. package/dist/capability-facts.d.ts +3 -1
  23. package/dist/capability-facts.js +1 -0
  24. package/dist/config-catalog.js +5 -1
  25. package/dist/config-types.d.ts +7 -3
  26. package/dist/config.js +7 -1
  27. package/dist/egress-redirect.d.ts +12 -0
  28. package/dist/egress-redirect.js +7 -0
  29. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  30. package/dist/entry-mode-clamp-notice.js +13 -0
  31. package/dist/exec-bounds.d.ts +69 -0
  32. package/dist/exec-bounds.js +37 -0
  33. package/dist/fleet/fleet-bus.d.ts +7 -0
  34. package/dist/fleet/fleet-bus.js +1 -0
  35. package/dist/hooks/hook-llm.js +6 -2
  36. package/dist/host-lsp-manager.d.ts +3 -3
  37. package/dist/http/admission.js +5 -2
  38. package/dist/http/resume-legs.js +46 -27
  39. package/dist/http/route-ctx.d.ts +1 -1
  40. package/dist/http/routes/approvals-assistant.js +6 -4
  41. package/dist/http/routes/capabilities.js +1 -0
  42. package/dist/http/routes/session-sync.js +3 -4
  43. package/dist/http/routes/sessions.js +4 -17
  44. package/dist/http/send.d.ts +1 -1
  45. package/dist/http/send.js +4 -3
  46. package/dist/http/wire-types.d.ts +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/observability/fail-open.d.ts +7 -3
  50. package/dist/observability/fail-open.js +7 -3
  51. package/dist/observability/secret-env-scrub.d.ts +37 -3
  52. package/dist/observability/secret-env-scrub.js +2 -2
  53. package/dist/parked-decide.js +5 -2
  54. package/dist/plugins/memory-embedder.js +5 -0
  55. package/dist/plugins/one-time-migrations.d.ts +180 -9
  56. package/dist/plugins/one-time-migrations.js +315 -3
  57. package/dist/plugins/pg-pool.js +3 -1
  58. package/dist/plugins/remote-env-e2b.d.ts +31 -9
  59. package/dist/plugins/remote-env-e2b.js +77 -26
  60. package/dist/plugins/remote-env-host.d.ts +17 -20
  61. package/dist/plugins/remote-env-host.js +20 -30
  62. package/dist/plugins/remote-env-k8s.d.ts +12 -4
  63. package/dist/plugins/remote-env-k8s.js +22 -31
  64. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  65. package/dist/plugins/session-policy-store-sql.js +17 -20
  66. package/dist/plugins/session-rules-column.d.ts +16 -0
  67. package/dist/plugins/session-rules-column.js +6 -0
  68. package/dist/plugins/tidb-pool.js +4 -1
  69. package/dist/read-face-posture.d.ts +2 -1
  70. package/dist/request-value-closure.js +1 -1
  71. package/dist/retired-card-words.d.ts +17 -0
  72. package/dist/retired-card-words.js +49 -0
  73. package/dist/rule-entry-list.js +2 -0
  74. package/dist/run-local.js +10 -3
  75. package/dist/server-secret-env.d.ts +77 -85
  76. package/dist/server-secret-env.js +40 -26
  77. package/dist/session-policy-wire.d.ts +61 -124
  78. package/dist/session-policy-wire.js +59 -121
  79. package/dist/session-sync.d.ts +8 -6
  80. package/dist/session-sync.js +6 -5
  81. package/dist/spec-fields.d.ts +3 -2
  82. package/dist/standing-answer-grant.d.ts +17 -9
  83. package/dist/standing-answer-grant.js +11 -8
  84. package/dist/task-settings.d.ts +6 -1
  85. package/dist/task-settings.js +1 -1
  86. package/dist/tool-approval.d.ts +14 -7
  87. package/dist/tool-approval.js +23 -18
  88. package/dist/trace/core-keyset-guard.d.ts +3 -3
  89. package/dist/trace/engine-notice-wire.d.ts +1 -1
  90. package/dist/trace/engine-notice-wire.js +1 -0
  91. package/dist/trace/project.d.ts +7 -4
  92. package/dist/trace/project.js +6 -75
  93. package/dist/trace/tool-output-media.d.ts +2 -1
  94. package/dist/trace/tool-output-media.js +3 -25
  95. package/dist/web-admin-client/apply-effective.js +28 -11
  96. package/dist/web-admin-client/facade.d.ts +1 -1
  97. package/dist/web-admin-client/facade.js +1 -1
  98. package/dist/web-admin-client/read-face.d.ts +73 -20
  99. package/dist/web-admin-client/read-face.js +76 -36
  100. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  101. package/dist/web-admin-client/skills-mcp.js +1 -1
  102. package/dist/web-admin-client/types.d.ts +3 -3
  103. package/package.json +3 -3
  104. package/dist/mcp-tool-axes.d.ts +0 -62
  105. package/dist/mcp-tool-axes.js +0 -47
@@ -3,6 +3,7 @@ import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCata
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError, sessionIdShapeIssue } from "../security.js";
5
5
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
+ import { buildEntryModeClampNotice } from "../entry-mode-clamp-notice.js";
6
7
  import { autonomyModeFloor } from "../runtime-governance.js";
7
8
  import { taskBodyKeyIssue } from "../request-key-closure.js";
8
9
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
@@ -629,7 +630,7 @@ export function createAdmission(ctx) {
629
630
  sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
630
631
  return null;
631
632
  }
632
- const admitted = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
633
+ const { body: admitted, entry: entryMode } = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
633
634
  try {
634
635
  const auth = deps.authorize ? await deps.authorize({ req, body: admitted }) : undefined;
635
636
  const engineNotices = [];
@@ -644,7 +645,9 @@ export function createAdmission(ctx) {
644
645
  });
645
646
  const model = modelIdOf(spec.model);
646
647
  const acceptance = createRunAcceptance({ sessionTitler: deps.sessionTitler }, { ...(spec.sessionId ? { sessionId: spec.sessionId } : {}), objective: spec.objective, ...(model !== undefined ? { model } : {}) });
647
- return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, acceptance, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
648
+ const clampNotice = buildEntryModeClampNotice(entryMode, spec.sessionId);
649
+ const pendingNotices = clampNotice !== undefined ? [clampNotice, ...engineNotices] : engineNotices;
650
+ return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, acceptance, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(pendingNotices.length > 0 ? { engineNotices: pendingNotices } : {}) };
648
651
  }
649
652
  catch (err) {
650
653
  if (err instanceof HttpError) {
@@ -49,9 +49,8 @@ import { buildCheckpointVersionKeys, resumeBlockedByPolicyAnswer, storedBodyRepl
49
49
  import { storedBodyReplayRefusal } from "./stored-body-replay.js";
50
50
  import { httpErrorCode } from "./send.js";
51
51
  import { createRunAcceptance } from "../run-acceptance.js";
52
- function isCheckpointReopenedFailure(r) {
53
- const code = failureCodeOf(r.terminal);
54
- return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
52
+ function legMayHaveReopenedCheckpoint(r) {
53
+ return r.terminal.kind === "failed" && nestedPauseOf(r.terminal) === undefined;
55
54
  }
56
55
  function nestedParkStatusOf(r) {
57
56
  const paused = nestedPauseOf(r.terminal);
@@ -476,6 +475,7 @@ export function createResumeLegs(ctx) {
476
475
  async function driveResumeLeg(args, onAccepted) {
477
476
  const resumeLegStartedAt = Date.now();
478
477
  const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
478
+ const reparkStatus = outcome.gate === "plan_review" ? "needs_review" : "suspended";
479
479
  const commitResumeCtx = () => deps.checkpointStore.putCtx(resumeCtx.sessionId, { ...resumeCtx.ctx, writerVersion: serviceVersion() }).catch((e) => {
480
480
  deps.metrics?.inc("resume_ctx_not_persisted_total");
481
481
  deps.logger?.warn?.("resume_ctx_not_persisted", { sessionId: resumeCtx.sessionId, gate: outcome.gate, error: errorText(e), note: "this continuation runs with the body it was given; the durable rebuild input still holds the PREVIOUS body, so the NEXT continuation of this session rebuilds from that one (never wider than this leg's)" });
@@ -562,6 +562,32 @@ export function createResumeLegs(ctx) {
562
562
  return true;
563
563
  }
564
564
  };
565
+ const settleCardForCancel = async () => {
566
+ const cs2 = deps.checkpointStore;
567
+ if (!cs2)
568
+ return;
569
+ try {
570
+ const live = await cs2.get(token);
571
+ if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
572
+ deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
573
+ }
574
+ }
575
+ catch (cancelSettleErr) {
576
+ deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
577
+ taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
578
+ });
579
+ }
580
+ };
581
+ const reopenedAfterLeg = async (leg, cancelledAtDone) => {
582
+ if (freshRunClaimed || !legMayHaveReopenedCheckpoint(leg))
583
+ return false;
584
+ const pending = cancelledAtDone ? false : await confirmCheckpointReopened();
585
+ if (cancelledAtDone || cancelCtrl.cause() !== undefined) {
586
+ await settleCardForCancel();
587
+ return false;
588
+ }
589
+ return pending;
590
+ };
565
591
  let resumeHeartbeat;
566
592
  let resumeManifestLease = 0;
567
593
  let resumeStreamRef;
@@ -685,7 +711,7 @@ export function createResumeLegs(ctx) {
685
711
  const vrNested = nestedPauseOf(vr.terminal);
686
712
  const reParkStatus = nestedParkStatusOf(vr);
687
713
  const reSuspended = vrNested !== undefined;
688
- const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
714
+ const vrReopened = await reopenedAfterLeg(vr, vrCause !== undefined);
689
715
  if (vrReopened)
690
716
  reopenConfirmedPark = true;
691
717
  if (taskId && deps.runStore) {
@@ -696,7 +722,7 @@ export function createResumeLegs(ctx) {
696
722
  if (reSuspended || vrReopened) {
697
723
  await appendModelUsageDelta(ap, deps.modelUsage, taskId);
698
724
  const vrParkCallId = vrNested !== undefined ? await parkToolCallId({ terminal: vrNested }, deps.checkpointStore) : undefined;
699
- await ap(reParkStatus ?? "suspended", { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
725
+ await ap(reParkStatus ?? reparkStatus, { gate: vrNested?.gate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: failureCodeOf(vr.terminal) ?? null } : {}) });
700
726
  }
701
727
  else {
702
728
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
@@ -819,8 +845,12 @@ export function createResumeLegs(ctx) {
819
845
  resumeStreamRef = stream;
820
846
  if (taskId)
821
847
  steerableRuns.set(taskId, stream);
822
- if (!taskId || !rs)
823
- return sealResultForExit(await stream.result());
848
+ if (!taskId || !rs) {
849
+ const rNoRow = await stream.result();
850
+ if (await reopenedAfterLeg(rNoRow, cancelCtrl.cause() !== undefined))
851
+ reopenConfirmedPark = true;
852
+ return sealResultForExit(rNoRow);
853
+ }
824
854
  onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
825
855
  let causeAtDone;
826
856
  for await (const ev of stream) {
@@ -951,10 +981,10 @@ export function createResumeLegs(ctx) {
951
981
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
952
982
  return r;
953
983
  }
954
- if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
984
+ if (await reopenedAfterLeg(r, causeAtDone !== undefined)) {
955
985
  reopenConfirmedPark = true;
956
986
  try {
957
- await append("suspended", { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
987
+ await append(reparkStatus, { gate: null, reopened: failureCodeOf(r.terminal) ?? null });
958
988
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
959
989
  }
960
990
  catch {
@@ -1000,20 +1030,7 @@ export function createResumeLegs(ctx) {
1000
1030
  const refusalCause = e.code === "checkpoint.invalid_outcome" ? decisionRefusalCauseOf(e, outcome.gate) : undefined;
1001
1031
  const disposition = resumeRefusalDisposition(e);
1002
1032
  if (disposition === "cancelled") {
1003
- const cs2 = deps.checkpointStore;
1004
- if (cs2) {
1005
- try {
1006
- const live = await cs2.get(token);
1007
- if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
1008
- deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
1009
- }
1010
- }
1011
- catch (cancelSettleErr) {
1012
- deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
1013
- taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
1014
- });
1015
- }
1016
- }
1033
+ await settleCardForCancel();
1017
1034
  if (claimedRow && taskId && deps.runStore) {
1018
1035
  const handleCause = cancelCtrl.cause();
1019
1036
  if (handleCause === undefined)
@@ -1035,8 +1052,8 @@ export function createResumeLegs(ctx) {
1035
1052
  settleFleet("failed");
1036
1053
  }
1037
1054
  else if (retriable) {
1038
- await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
1039
- settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
1055
+ await (reparkStatus === "needs_review" ? deps.runStore.setNeedsReview(taskId, null) : deps.runStore.setSuspended(taskId, null)).catch(() => undefined);
1056
+ settleFleet(reparkStatus);
1040
1057
  }
1041
1058
  else {
1042
1059
  await deps.runStore.setTerminal(taskId, "failed", null, e.message).catch(() => undefined);
@@ -1099,14 +1116,16 @@ export function createResumeLegs(ctx) {
1099
1116
  const sealed = sealResultForExit(result);
1100
1117
  const resumePlane = outwardPlane(sealed.terminal);
1101
1118
  if (taskId && deps.runStore) {
1102
- if (resumePlane.status === "suspended" || checkpointReopened)
1119
+ if (checkpointReopened && reparkStatus === "needs_review")
1120
+ await deps.runStore.setNeedsReview(taskId, null);
1121
+ else if (resumePlane.status === "suspended" || checkpointReopened)
1103
1122
  await deps.runStore.setSuspended(taskId, sealed.terminal.kind === "paused" ? sealed : null);
1104
1123
  else if (resumePlane.status === "needs_review")
1105
1124
  await deps.runStore.setNeedsReview(taskId, sealed);
1106
1125
  else
1107
1126
  await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
1108
1127
  }
1109
- settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
1128
+ settleFleet(checkpointReopened ? reparkStatus : resumePlane.status, fleetRunResiduals(sealed));
1110
1129
  return {
1111
1130
  status: 200,
1112
1131
  decisionCommitted: true,
@@ -128,7 +128,7 @@ export interface PreparedTaskSubmission {
128
128
  cascade?: boolean;
129
129
  jobId?: string;
130
130
  body: TaskRequestBody;
131
- /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天只有请求腿 MCP 注入被丢弃一族)。
131
+ /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天两族:请求腿 MCP 注入被丢弃;入口地板钳制 `permission.entry_mode_clamped`)。
132
132
  * 装配跑在这条腿的通告口开张之前,所以铸点交不出去 —— 由腿在 `registerEngineNoticeLeg` 那一拍
133
133
  * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
134
134
  engineNotices?: readonly EngineNotice[];
@@ -18,7 +18,7 @@ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
20
  import { deploymentListAsks } from "../../approval.js";
21
- import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
+ import { callGrantBindingOf, questionGrantBindingOf } from "../../standing-answer-grant.js";
22
22
  import { sendResumeOutcome } from "../route-ctx.js";
23
23
  import { assertNever } from "../route-ctx.js";
24
24
  import * as facts from "../../capability-facts.js";
@@ -321,7 +321,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
321
321
  return;
322
322
  }
323
323
  const rows = await deps.approvalExemptionStore.list(exSessionId);
324
- sendJson(res, 200, { exemptions: rows.filter((r) => !answerGrantsBeyondCall({ toolName: r.toolName })) });
324
+ sendJson(res, 200, { exemptions: rows.filter((r) => callGrantBindingOf({ toolName: r.toolName }).kind === "none") });
325
325
  return;
326
326
  }
327
327
  if (exToolName === undefined) {
@@ -360,6 +360,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
360
360
  ...(s.boundGrantDigest !== undefined ? { boundGrantDigest: s.boundGrantDigest } : {}),
361
361
  ...(s.boundGrants !== undefined ? { boundGrants: s.boundGrants } : {}),
362
362
  ...(s.classifierNoRuling !== undefined ? { classifierNoRuling: s.classifierNoRuling } : {}),
363
+ ...(s.boundGrantAbsence !== undefined ? { boundGrantAbsence: s.boundGrantAbsence } : {}),
363
364
  objective,
364
365
  input,
365
366
  };
@@ -840,13 +841,14 @@ function projectPendingForWire(row, deployment) {
840
841
  const governance = deployment;
841
842
  const redacted = redactPendingDisclosures(row);
842
843
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
844
+ const deploymentListQuestion = carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes);
843
845
  const rememberRefused = !standingAnswerDurablyRecordable({
844
846
  governanceForced: governanceForced === true,
845
847
  requiresRealApproval: row.requiresRealApproval === true,
846
848
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
847
- grantBinding: grantBindingOf(row),
849
+ grantBinding: questionGrantBindingOf(row, deploymentListQuestion),
848
850
  classes: row.classes,
849
- deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
851
+ deploymentListQuestion,
850
852
  });
851
853
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
852
854
  }
@@ -73,6 +73,7 @@ export async function handleCapabilities(req, res, match, ctx) {
73
73
  const rulesLaneForCaller = facts.ruleLaneForCaller(deps, gatedPrincipal(req, deps.config)).available;
74
74
  sendJson(res, 200, gateCompositeMembers({
75
75
  ...deps.capabilities,
76
+ ...facts.engineCapabilityBits(),
76
77
  asyncRuns: facts.durableRuns(deps).available,
77
78
  artifacts: facts.durableRuns(deps).available,
78
79
  approvals: facts.checkpointPark(deps).available,
@@ -14,7 +14,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
14
14
  const SYNC_IMPORT_BATCH = 500;
15
15
  import { SHA256_HEX_RE } from "../../digest-form.js";
16
16
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
17
- import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
17
+ import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
18
18
  import { readSessionPinned, readSessionTitle } from "../../session-row.js";
19
19
  export function createSessionSyncLocal() {
20
20
  const pendingStagings = new Map();
@@ -232,9 +232,8 @@ export async function handleSessionSync(req, res, match, ctx) {
232
232
  }
233
233
  const policyRead = readSessionRulesRecords(body.policy);
234
234
  if (policyRead.problem !== undefined) {
235
- const legacy = legacyToolNameProblemOf(policyRead.problem);
236
- if (legacy !== undefined) {
237
- sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { ...legacy });
235
+ if (policyRead.problem.kind === "rules") {
236
+ sendSessionPolicyError(res, policyRead.problem.refusal, `policy: [${policyRead.problem.index}].rules: `);
238
237
  return;
239
238
  }
240
239
  sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
@@ -7,7 +7,7 @@ import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
8
8
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
9
9
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
10
- import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
10
+ import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
11
11
  import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
12
12
  import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
13
13
  import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
@@ -819,22 +819,9 @@ export async function handleSessions(req, res, match, ctx) {
819
819
  return;
820
820
  }
821
821
  const read = readWritableSessionPermissionRules(body.rules);
822
- if (read.problem !== undefined) {
823
- switch (read.problem.kind) {
824
- case "not-an-object":
825
- sendError(res, 400, "request.body_shape", describeSessionPermissionRulesProblem(read.problem));
826
- return;
827
- case "field-not-rule-list":
828
- sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
829
- return;
830
- case "legacy-tool-name":
831
- sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { ...read.problem.refusal });
832
- return;
833
- default: {
834
- const unhandled = read.problem;
835
- throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
836
- }
837
- }
822
+ if (read.refusal !== undefined) {
823
+ sendSessionPolicyError(res, read.refusal);
824
+ return;
838
825
  }
839
826
  if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
840
827
  sendError(res, 400, "request.field_invalid", "expectedRev must be a non-negative integer");
@@ -84,7 +84,7 @@ export declare function httpErrorCode(status: number, code?: string): string;
84
84
  * 域外通用臂 ⇒ `500 internal.error "internal error"`:码丢,连带**修复路径**也丢(同一行 `PUT` 却答
85
85
  * `500 corrupt` 并点名那一列)。一只腿之后,新站点只会忘记调它,不会再各自发明一份映射。
86
86
  */
87
- export declare function sendSessionPolicyError(res: ServerResponse, e: unknown): boolean;
87
+ export declare function sendSessionPolicyError(res: ServerResponse, e: unknown, sentencePrefix?: string): boolean;
88
88
  /**
89
89
  * S-612 —— core `SessionError` → 应答,经 {@link sessionErrorHttp} 那**一只**映射(`src/session-error-wire.ts`)。
90
90
  * 回 `true` = 已应答,调用点 `return`;`false` = 不是 `SessionError`,或这个词在这一面上是登记过的留白(站点的既有
package/dist/http/send.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SessionError, SessionPolicyError } from "@sema-agent/core";
2
- import { sessionPolicyErrorHttp } from "../session-policy-wire.js";
2
+ import { sessionPolicyErrorFacts, sessionPolicyErrorHttp, sessionPolicyErrorSentence } from "../session-policy-wire.js";
3
3
  import { sessionErrorHttp } from "../session-error-wire.js";
4
4
  import { redactErrorMessage } from "../observability/run-terminal.js";
5
5
  export const SSE_RESPONSE_MARK = Symbol("sema.http.sse-response");
@@ -48,11 +48,12 @@ export function httpErrorCode(status, code) {
48
48
  default: return status >= 500 ? "internal.error" : "request.rejected";
49
49
  }
50
50
  }
51
- export function sendSessionPolicyError(res, e) {
51
+ export function sendSessionPolicyError(res, e, sentencePrefix = "") {
52
52
  if (!(e instanceof SessionPolicyError))
53
53
  return false;
54
54
  const h = sessionPolicyErrorHttp(e.code);
55
- sendError(res, h.status, h.errorCode, redactErrorMessage(e.message) ?? "session policy error");
55
+ const facts = sessionPolicyErrorFacts(e);
56
+ sendError(res, h.status, h.errorCode, redactErrorMessage(`${sentencePrefix}${sessionPolicyErrorSentence(e)}`) ?? "session policy error", facts === undefined ? undefined : { ...facts });
56
57
  return true;
57
58
  }
58
59
  export function sendSessionError(res, e, face) {
@@ -312,7 +312,8 @@ export interface TaskRequestBody {
312
312
  excludeTools?: string[];
313
313
  /** S-1448(core 7.49.0 #1623,[ref]):引擎的**可用性选择器** —— 这一 run 只用列出的工具(wire 名、别名、`Tool(content)` 整只、
314
314
  * `mcp__srv` / `mcp__srv__*` / 工具段通配;`"*"` = 本层不设名单;`[]` = 什么都不给)。宿主**原样透传**进 `TaskSpec.allowTools`:
315
- * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
315
+ * 不算补集、不过滤、不与 `settings.permissions.allow`(免问)混词;没有任何工具答应的名字照跑(原样保留,今天什么都不准,之后挂上同名工具即生效;引擎告知运维一次),
316
+ * 任何工具名都不可能有的拼法由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。
316
317
  * `excludeTools` 是同一只运算的减项(deny 赢)。值形:字符串数组(受理门 400 `request.field_invalid`,与 `excludeTools` 同一只门);
317
318
  * 装着的引擎不认该座 ⇒ 400 `request.field_invalid` 点名所需引擎版本(不静默丢)。无租户门(收窄)。 */
318
319
  allowTools?: string[];
@@ -415,7 +416,7 @@ export interface TaskRequestBody {
415
416
  /**
416
417
  * S-895(7.106.0,core [ref] [ref] ②)—— 模型驱动的 host shell **继承多少用户环境**,由调用方按请求声明。
417
418
  * 闭集两词(词表外 ⇒ 400 `request.field_invalid`,两腿同判,判据属主 `request-value-closure.ts` 的 `inheritEnvFieldIssue`):
418
- * · `"all"` —— 整份继承(`{ ...process.env }` 逐字原样,CC 交互形);只在**单用户**部署兑现,射程 = 写容纳面那一只
419
+ * · `"all"` —— 整份用户环境**减本服务自有凭据名**(配置目录 secret 名 / 名单型旋钮名 / 模型条目 `apiKeyEnv`,起服向引擎声明一次,判在引擎;7.129.0 起,CC 交互形);只在**单用户**部署兑现,射程 = 写容纳面那一只
419
420
  * `hostPostureDeclarationsHonored`(`hosted-posture.ts`)⇒ 托管部署(`REQUIRE_PRINCIPAL=true`)响亮拒
420
421
  * 422 `request.host_posture_refused`;
421
422
  * · `"scrub"` —— 缺省清洗(剥凭据形变量),**任何车道、任何部署**都受理(它就是缺省;7.106.0 合并复审 X7)。
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export { createAuthorizer, principalFrom, HttpError, type AuthContext, type Owne
24
24
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
25
25
  export type { TaskRequestBody } from "./http/wire-types.js";
26
26
  export { createHttpServer, type ServiceDeps, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
27
- export { createSideQueryLane, createPerModelKeyBrain, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
27
+ export { createSideQueryLane, type KeyResolver, type SideQueryLane, type SideQueryLaneCtx } from "./boot/side-query-lane.js";
28
28
  export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
29
29
  export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type ModelKeyPoisonReason } from "./key-resolver.js";
30
30
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task
21
21
  export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
22
22
  export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
23
23
  export { createHttpServer, } from "./http/server.js";
24
- export { createSideQueryLane, createPerModelKeyBrain } from "./boot/side-query-lane.js";
24
+ export { createSideQueryLane } from "./boot/side-query-lane.js";
25
25
  export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
26
26
  export { isModelKeyPoison, ModelKeyPoisonedError } from "./key-resolver.js";
27
27
  export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES } from "./model-entry-headers.js";
@@ -43,9 +43,9 @@ export declare const FAIL_OPEN_TAGS: {
43
43
  readonly cls: "F";
44
44
  readonly note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节(`output` 媒体块的 `data` 与 `structured` 媒体卡的 `file.base64` 一起计)+ 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体降成持久面那只形(媒体块换描述符 `{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`;媒体卡去掉 `file.base64`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。";
45
45
  };
46
- readonly "server.memory-consolidation.operator-valve-unjudged": {
46
+ readonly "server.memory-consolidation.operator-valve-layers-unfed": {
47
47
  readonly cls: "P-DEBT";
48
- readonly note: "core 7.50.0 #1653 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门这条腿的写判官座答 `no_objection` —— 这条腿没有腿(无名册写脸、无引擎盖的豁免根、无人的持久规则车道),引擎包不导出它那只合成写判官的构造器,本仓不手搭第二只。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的部署写策略或人的持久规则拦不住这一轮折叠(与引擎 7.49.x 逐字同:那时驱动根本不问);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 引擎导出宿主可用的写判官构造器,换上即删本 tag。";
48
+ readonly note: "core 7.51.0 #1718 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门的写判官座已是引擎的那一只构造器,但三层没喂 —— 人的持久规则层(阀门没有「库属于谁」的来源,无 principal / 不声明 local-owner)、部署的调用方策略层(`APPROVAL_REQUIRE` / `APPROVAL_DENY` / `AUTONOMY` / 敏感路径守卫集是每任务现合成的活配置、无部署级单例;敏感路径层另需引擎豁免根入参,宿主构造器输入没有这一位)、项目根(项目层 scope 键是任务根的单向散列,反推不出)。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的人的持久规则或部署写策略(例如 `APPROVAL_DENY=Write`)拦不住这一轮折叠(与 7.128.x 逐字同;只是判官不再是代答);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 三层各自有了来源(人:「读谁的桶」裁定;策略:一份设计 + 向 core 请托引擎豁免根入参;根:无来源则永久缺席并改写本债)那一版,本 tag 删。";
49
49
  };
50
50
  readonly "server.workflow-park-index.sync-failed": {
51
51
  readonly cls: "P-DEBT";
@@ -261,7 +261,7 @@ export declare const FAIL_OPEN_TAGS: {
261
261
  };
262
262
  readonly "server.resume.reopen-confirm-unreadable": {
263
263
  readonly cls: "F";
264
- readonly note: "S-447(#60 M2 自查项,`http/resume-legs.ts` 的 `confirmCheckpointReopened`):续跑腿在 core 给出 reopen 族失败后按 **token** 复核「这张 checkpoint 是不是又 pending 了」,这一读抛(库拖/版本太新/店不可达)。**方向本来就是 fail-closed**:读不出 ⇒ `return true` ⇒ 「不写任何终局、重新 park」(`:1206-1212` 头注已把取舍与有界代价写死:错 park 由 `reapSuspended` 兑,错终局会孤儿化一张活 park 到 TTL)—— 所以这条腿不是 P 类,缺的只是**留痕**。放行的最坏后果 = 多一次多余的 re-park(一张已经被决掉的卡重新挂到 reaper 窗里),人面上是「这条续跑没动」而不是谁被放行了。必须登记的理由是 **uniformity**:同文件三只同形读(`:249` `server.approvals.stale-echoed-token-unreadable` · `:288` `server.approvals.stale-current-pending-unreadable` · 本腿)中只有它是黑的,于是「店一直读不出所以每条续跑都在白白 re-park」与「一切正常」在遥测上同形。计数非零 = 这台机器的 checkpoint 店在拖,运维要去看库。";
264
+ readonly note: "S-447(#60 M2 自查项,`http/resume-legs.ts` 的 `confirmCheckpointReopened`):续跑腿交回一条没 park 的失败结果后(7.129.0 起每一条都问,不再只在三个重开码上问)按 **token** 复核「这张 checkpoint 是不是又 pending 了」,这一读抛(库拖/版本太新/店不可达)。**方向本来就是 fail-closed**:读不出 ⇒ `return true` ⇒ 「不写任何终局、重新 park」(`:1206-1212` 头注已把取舍与有界代价写死:错 park 由 `reapSuspended` 兑,错终局会孤儿化一张活 park 到 TTL)—— 所以这条腿不是 P 类,缺的只是**留痕**。放行的最坏后果 = 多一次多余的 re-park(一张已经被决掉的卡重新挂到 reaper 窗里),人面上是「这条续跑没动」而不是谁被放行了。必须登记的理由是 **uniformity**:同文件三只同形读(`:249` `server.approvals.stale-echoed-token-unreadable` · `:288` `server.approvals.stale-current-pending-unreadable` · 本腿)中只有它是黑的,于是「店一直读不出所以每条续跑都在白白 re-park」与「一切正常」在遥测上同形。计数非零 = 这台机器的 checkpoint 店在拖,运维要去看库。";
265
265
  };
266
266
  readonly "server.approvals.stale-current-pending-unreadable": {
267
267
  readonly cls: "F";
@@ -327,6 +327,10 @@ export declare const FAIL_OPEN_TAGS: {
327
327
  readonly cls: "F";
328
328
  readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天四个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛)、S-1170 起的 `classifierNoRuling`(core 7.43.0 #1353 的成因词,活卡帧读 `AskRequest.classifierNoRuling`,闭集筛 = core `isAutoModeFallbackCause`)与 `differs`(core 7.44.0 #1374,载体是 `runs.ts` 的 409 `steering.duplicate_input_id` 体读的引擎拒错 `detail.differs`,闭集筛 = core `STEER_REPLAY_FACETS` 的非空子集;集外成员 / 空 / 非数组 ⇒ 不铸 `detail`,码仍是契约);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` / `classifierNoRuling=` / `differs=` 判别是哪一座;第四个成员是 `approval-content-kind.ts` 的 `readApprovalCard` —— 卡词 `approvalCard` 自己的闭集窄读,活 ask / LOCAL 店停驻门 / SQL 店读出的门子树三个载体共用一只,闭集 = core `TOOL_APPROVAL_CARDS`,detail 前缀 `approvalCard=`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
329
329
  };
330
+ readonly "server.approval-card.retired-card-word-read": {
331
+ readonly cls: "F";
332
+ readonly note: "S-1562(core 7.51.0 #1515 / #1702;一次性迁移 M18 的**读侧半场**,`approval-card.ts` 的 `readStoredCardEnvelope` / `currentParkArm`,窗与删法见 `plugins/one-time-migrations.ts` 登记表 M18 —— 窗关随 M18 删本 tag):存下的审批卡(`approval_ask.card_json`)或停驻行(`pendingAction` 的 `tool_approval` 臂)上带着引擎 7.51.0 退役的词 —— `ruleOffersAbsence: \"shadowed\"` 或类对里的 `governance_unavailable` —— 读时按 M18 那一只改写读(前者删键 = 合法缺席,后者换成 `rule_unreadable`、类集去重按引擎序)。每改写一次记一次;detail = 载体(`card_json` / `parked row`)与座名,不带值。放行的最坏后果 = 无:这一读与存卡的起服改写对同一份字节给同一个答案(一只函数);它不是兜底,它登记在这里是为了**响亮** —— 载体 `card_json` 计数非零 = 有起服改写够不着的卡(滚动升级窗里 7.128.x 副本新写的卡、local 文件 ask 账);载体 `parked row` 计数非零 = 读到了带旧词的停驻行(引擎写的文档,本服务从不改写它 —— 行摘要覆盖类对 `askClass` / `classes`,改写 = 续跑判 `damaged`;所以这一格在窗内随每次读到那样的行而涨,直到那些行被决掉 / 收割),运维据它判断旧副本是否都已换下、窗关删码前是否还有这种行在飞。反方向(不改写)= 修前:整张存卡读不出(断连重放跳过、幂等重入走 park、回决口按要人)、停驻行的 `shadowed` 读成「读不出 ⇒ 按强制」(拿不到「本会话放行」)。";
333
+ };
330
334
  readonly "server.pg-pool.idle-client-error": {
331
335
  readonly cls: "F";
332
336
  readonly note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。";
@@ -24,9 +24,9 @@ export const FAIL_OPEN_TAGS = {
24
24
  cls: "F",
25
25
  note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节(`output` 媒体块的 `data` 与 `structured` 媒体卡的 `file.base64` 一起计)+ 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体降成持久面那只形(媒体块换描述符 `{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`;媒体卡去掉 `file.base64`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。",
26
26
  },
27
- "server.memory-consolidation.operator-valve-unjudged": {
27
+ "server.memory-consolidation.operator-valve-layers-unfed": {
28
28
  cls: "P-DEBT",
29
- note: "core 7.50.0 #1653 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门这条腿的写判官座答 `no_objection` —— 这条腿没有腿(无名册写脸、无引擎盖的豁免根、无人的持久规则车道),引擎包不导出它那只合成写判官的构造器,本仓不手搭第二只。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的部署写策略或人的持久规则拦不住这一轮折叠(与引擎 7.49.x 逐字同:那时驱动根本不问);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 引擎导出宿主可用的写判官构造器,换上即删本 tag。",
29
+ note: "core 7.51.0 #1718 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门的写判官座已是引擎的那一只构造器,但三层没喂 —— 人的持久规则层(阀门没有「库属于谁」的来源,无 principal / 不声明 local-owner)、部署的调用方策略层(`APPROVAL_REQUIRE` / `APPROVAL_DENY` / `AUTONOMY` / 敏感路径守卫集是每任务现合成的活配置、无部署级单例;敏感路径层另需引擎豁免根入参,宿主构造器输入没有这一位)、项目根(项目层 scope 键是任务根的单向散列,反推不出)。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的人的持久规则或部署写策略(例如 `APPROVAL_DENY=Write`)拦不住这一轮折叠(与 7.128.x 逐字同;只是判官不再是代答);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 三层各自有了来源(人:「读谁的桶」裁定;策略:一份设计 + 向 core 请托引擎豁免根入参;根:无来源则永久缺席并改写本债)那一版,本 tag 删。",
30
30
  },
31
31
  "server.workflow-park-index.sync-failed": {
32
32
  cls: "P-DEBT",
@@ -242,7 +242,7 @@ export const FAIL_OPEN_TAGS = {
242
242
  },
243
243
  "server.resume.reopen-confirm-unreadable": {
244
244
  cls: "F",
245
- note: "S-447(#60 M2 自查项,`http/resume-legs.ts` 的 `confirmCheckpointReopened`):续跑腿在 core 给出 reopen 族失败后按 **token** 复核「这张 checkpoint 是不是又 pending 了」,这一读抛(库拖/版本太新/店不可达)。**方向本来就是 fail-closed**:读不出 ⇒ `return true` ⇒ 「不写任何终局、重新 park」(`:1206-1212` 头注已把取舍与有界代价写死:错 park 由 `reapSuspended` 兑,错终局会孤儿化一张活 park 到 TTL)—— 所以这条腿不是 P 类,缺的只是**留痕**。放行的最坏后果 = 多一次多余的 re-park(一张已经被决掉的卡重新挂到 reaper 窗里),人面上是「这条续跑没动」而不是谁被放行了。必须登记的理由是 **uniformity**:同文件三只同形读(`:249` `server.approvals.stale-echoed-token-unreadable` · `:288` `server.approvals.stale-current-pending-unreadable` · 本腿)中只有它是黑的,于是「店一直读不出所以每条续跑都在白白 re-park」与「一切正常」在遥测上同形。计数非零 = 这台机器的 checkpoint 店在拖,运维要去看库。",
245
+ note: "S-447(#60 M2 自查项,`http/resume-legs.ts` 的 `confirmCheckpointReopened`):续跑腿交回一条没 park 的失败结果后(7.129.0 起每一条都问,不再只在三个重开码上问)按 **token** 复核「这张 checkpoint 是不是又 pending 了」,这一读抛(库拖/版本太新/店不可达)。**方向本来就是 fail-closed**:读不出 ⇒ `return true` ⇒ 「不写任何终局、重新 park」(`:1206-1212` 头注已把取舍与有界代价写死:错 park 由 `reapSuspended` 兑,错终局会孤儿化一张活 park 到 TTL)—— 所以这条腿不是 P 类,缺的只是**留痕**。放行的最坏后果 = 多一次多余的 re-park(一张已经被决掉的卡重新挂到 reaper 窗里),人面上是「这条续跑没动」而不是谁被放行了。必须登记的理由是 **uniformity**:同文件三只同形读(`:249` `server.approvals.stale-echoed-token-unreadable` · `:288` `server.approvals.stale-current-pending-unreadable` · 本腿)中只有它是黑的,于是「店一直读不出所以每条续跑都在白白 re-park」与「一切正常」在遥测上同形。计数非零 = 这台机器的 checkpoint 店在拖,运维要去看库。",
246
246
  },
247
247
  "server.approvals.stale-current-pending-unreadable": {
248
248
  cls: "F",
@@ -287,6 +287,10 @@ export const FAIL_OPEN_TAGS = {
287
287
  cls: "F",
288
288
  note: "S-249(`approval-card.ts` 的边界窄读族;今天四个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛)、S-1170 起的 `classifierNoRuling`(core 7.43.0 #1353 的成因词,活卡帧读 `AskRequest.classifierNoRuling`,闭集筛 = core `isAutoModeFallbackCause`)与 `differs`(core 7.44.0 #1374,载体是 `runs.ts` 的 409 `steering.duplicate_input_id` 体读的引擎拒错 `detail.differs`,闭集筛 = core `STEER_REPLAY_FACETS` 的非空子集;集外成员 / 空 / 非数组 ⇒ 不铸 `detail`,码仍是契约);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` / `classifierNoRuling=` / `differs=` 判别是哪一座;第四个成员是 `approval-content-kind.ts` 的 `readApprovalCard` —— 卡词 `approvalCard` 自己的闭集窄读,活 ask / LOCAL 店停驻门 / SQL 店读出的门子树三个载体共用一只,闭集 = core `TOOL_APPROVAL_CARDS`,detail 前缀 `approvalCard=`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
289
289
  },
290
+ "server.approval-card.retired-card-word-read": {
291
+ cls: "F",
292
+ note: "S-1562(core 7.51.0 #1515 / #1702;一次性迁移 M18 的**读侧半场**,`approval-card.ts` 的 `readStoredCardEnvelope` / `currentParkArm`,窗与删法见 `plugins/one-time-migrations.ts` 登记表 M18 —— 窗关随 M18 删本 tag):存下的审批卡(`approval_ask.card_json`)或停驻行(`pendingAction` 的 `tool_approval` 臂)上带着引擎 7.51.0 退役的词 —— `ruleOffersAbsence: \"shadowed\"` 或类对里的 `governance_unavailable` —— 读时按 M18 那一只改写读(前者删键 = 合法缺席,后者换成 `rule_unreadable`、类集去重按引擎序)。每改写一次记一次;detail = 载体(`card_json` / `parked row`)与座名,不带值。放行的最坏后果 = 无:这一读与存卡的起服改写对同一份字节给同一个答案(一只函数);它不是兜底,它登记在这里是为了**响亮** —— 载体 `card_json` 计数非零 = 有起服改写够不着的卡(滚动升级窗里 7.128.x 副本新写的卡、local 文件 ask 账);载体 `parked row` 计数非零 = 读到了带旧词的停驻行(引擎写的文档,本服务从不改写它 —— 行摘要覆盖类对 `askClass` / `classes`,改写 = 续跑判 `damaged`;所以这一格在窗内随每次读到那样的行而涨,直到那些行被决掉 / 收割),运维据它判断旧副本是否都已换下、窗关删码前是否还有这种行在飞。反方向(不改写)= 修前:整张存卡读不出(断连重放跳过、幂等重入走 park、回决口按要人)、停驻行的 `shadowed` 读成「读不出 ⇒ 按强制」(拿不到「本会话放行」)。",
293
+ },
290
294
  "server.pg-pool.idle-client-error": {
291
295
  cls: "F",
292
296
  note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。",
@@ -1,4 +1,38 @@
1
- import type { ServerSecretEnvFinding } from "../server-secret-env.js";
1
+ /**
2
+ * [ref]([ref])—— **密钥形 env 剥除的留痕面**(core D1 `scrubSecretEnv` 的可观测半场)。
3
+ *
4
+ * ## 它解决的问题
5
+ * `scrubSecretEnv` 是本仓两条 shell/子进程腿的**安全默认**:模型可驱动的 shell(`inheritEnv:"scrub"`)与
6
+ * git 子进程(钩子/凭证助手)拿到的 env 里,`*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD`… 一律先丢掉。
7
+ * 两处都以**单参形**调用,于是「这一次到底丢了哪些 key、按哪条规则(core 7.35.0 起五词:exact-name / prefix-rule / namespace-rule / name-rule /
8
+ * value-rule)、置信度多少」
9
+ * **零留痕** —— 部署方既证明不了保护生效过,也发现不了它悄悄不生效了(词表漂移 / 一个新的密钥命名法从
10
+ * 规则的缝里穿过去 / 某条腿被改成 `inheritEnv:"all"`)。core 早就把收集口开好了(`findings?:
11
+ * SecretEnvFinding[]`,d.ts 原话:the VALUE is never carried — this record is meant to be loggable),
12
+ * 缺的只是本仓这半场。本模块就是那半场,**不是**一个新的执法面:它一个字节也不改变剥除行为。
13
+ *
14
+ * ## 形(照 `fail-open.ts` 的仓内先例)
15
+ * 1. **计数(逐次)** —— `secret_env_scrubbed_total{site,kind,confidence}`;
16
+ * 2. **结构化披露(每 site×key 一次)** —— 首次在某条腿上丢掉某个 key 时一条 `info`,之后同 key 只走计数。
17
+ * 有部署级出路的现场(S-699:`server.lsp.spawn-env`;7.103.0 起 + `server.bake-runner.spawn-env`)升 `warn`,并带当前放行名单与出路句
18
+ * —— 规则见 `SecretEnvScrubSiteSpec`。
19
+ * 不是「每 site 一次」:一个**新**的密钥形 key 后来才出现(部署加了一个凭证)时,那才是最该看见的一行。
20
+ * findings 全部是 core 的 `SecretEnvFinding`(7.129.0 / core 7.51.1 起只有引擎一只写手,经 `server-secret-env.ts` 的唯一组合):形状臂的五词
21
+ * `exact-name` / `prefix-rule`(`OTEL_*`)/ `namespace-rule`(`SEMA_` 命名空间)/ `name-rule`(名字形状)/ `value-rule`(值里带凭据),加
22
+ * `own-credential`(本进程起服声明的自有凭据名 —— 配置目录 secret 名 ∪ 名单型旋钮名 —— 被形状臂漏下、由声明臂扣的;`all` 词下也扣)。
23
+ * `kind` 原样进标签、不做穷举 switch,core 换词即自成新桶;本仓 7.99.0–7.128.x 自铸的 `config-catalog` / `name-list-knob` 两桶随目录臂退役,
24
+ * 它们的读数今天在 `own-credential` 桶 —— 那一桶就是 core 形状规则的缺口读数。git 的 env 配置族与代理变量一族自 core 7.35.0 起是**豁免族**
25
+ * (名与值都不读、原样交出),不产 finding;此前(7.33.2–7.33.8)被扣的成员记 core 的 `family-rule`,本仓 7.103.0 的 `git-config-group` 桶与 7.103.1
26
+ * 中间态的 `secret_env_git_config_pairs_withheld_total` / warn `git_config_pair_withheld` 更早随本仓那一层退役(车PG)。
27
+ * 值永不进任何一件(core 的 finding 本身就不携带值)。
28
+ *
29
+ * ## 为什么是进程级自由函数而不是注入
30
+ * 与 `recordFailOpen` 同因:调用点都在**没有 logger/metrics 句柄**的深处(`plugins/remote-env-host.ts` 的 `mergeEnv` 全文零观测通道;
31
+ * git 腿是自由函数 / 闭包)。7.103.0 起各腿不再直接调本函数 —— 只经 `server-secret-env.ts` 的唯一组合 `hostSubprocessEnv`。
32
+ * 装配层用 {@link installSecretEnvScrubRecorder} 把句柄接上,装配之前命中的次数不会丢(计数照常累加,
33
+ * install 时一次性补进 metrics),披露行则在下一次命中时补发(未成功披露的 key 不进 `disclosed` 集)。
34
+ */
35
+ import { type SecretEnvFinding } from "@sema-agent/core";
2
36
  import type { Logger } from "./logger.js";
3
37
  import type { Metrics } from "./metrics.js";
4
38
  /**
@@ -16,7 +50,7 @@ export interface SecretEnvScrubSiteSpec {
16
50
  /** 登记过的剥除现场。闭集:未登记的 site 传不进 {@link recordSecretEnvScrub}(编译期拒,同 FAIL_OPEN_TAGS 纪律)。 */
17
51
  export declare const SECRET_ENV_SCRUB_SITES: {
18
52
  readonly "server.remote-env-host.inherit-scrub": {
19
- readonly note: "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令";
53
+ readonly note: "模型可驱动的 host shell 继承 env(`inheritEnv` 三词任一,缺省 `\"scrub\"`;`\"all\"` 词下扣的只有本服务自己的凭据名)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令";
20
54
  readonly remedy: null;
21
55
  };
22
56
  readonly "server.project-memory.git-subprocess": {
@@ -52,7 +86,7 @@ interface Sink {
52
86
  * `findings` 只许装**没有按原样交给子进程**的键(剥掉 / 换成 core 占位半条)—— 点名放回的键取回源值,不是剥除(不计数、不披露)。
53
87
  * `allowed` = 这一次生效的点名放行名单(S-699),只在有出路的现场(`remedy` 在场)写进披露行;没有放行面的腿传 `[]`。
54
88
  */
55
- export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly ServerSecretEnvFinding[], allowed: readonly string[]): void;
89
+ export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly SecretEnvFinding[], allowed: readonly string[]): void;
56
90
  /** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。装配前累计的次数在此补进 metrics。 */
57
91
  export declare function installSecretEnvScrubRecorder(deps: Sink): void;
58
92
  /** 已留驻的「现场×键」条数(测试/诊断读面)。恒 ≤ {@link DISCLOSED_CAP}。 */
@@ -2,7 +2,7 @@ import { summarizeRedactions } from "@sema-agent/core";
2
2
  import { describeAllowlistPostures } from "../env-name-allowlist-knobs.js";
3
3
  export const SECRET_ENV_SCRUB_SITES = {
4
4
  "server.remote-env-host.inherit-scrub": {
5
- note: "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令",
5
+ note: "模型可驱动的 host shell 继承 env(`inheritEnv` 三词任一,缺省 `\"scrub\"`;`\"all\"` 词下扣的只有本服务自己的凭据名)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令",
6
6
  remedy: null,
7
7
  },
8
8
  "server.project-memory.git-subprocess": {
@@ -27,7 +27,7 @@ export const SECRET_ENV_SCRUB_SITES = {
27
27
  },
28
28
  };
29
29
  const SCRUB_COUNTER = "secret_env_scrubbed_total";
30
- const SCRUB_COUNTER_HELP = "secret env entries not handed on as written (dropped) to a spawned child process, by site/kind/confidence (core scrubSecretEnv rules + server config-catalog secret names + name-list knobs)";
30
+ const SCRUB_COUNTER_HELP = "secret env entries not handed on as written (dropped) to a spawned child process, by site/kind/confidence (core inheritedHostEnv: the scrub's rules + this deployment's declared own credentials, kind own-credential)";
31
31
  const DISCLOSED_CAP = 512;
32
32
  const SEP = "=";
33
33
  let sink = {};
@@ -1,3 +1,4 @@
1
+ import { readClosedWordSeat } from "./approval-card.js";
1
2
  import { routeWorkflowPark } from "./orchestration/workflow-agent-session-index.js";
2
3
  import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
3
4
  import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
@@ -313,8 +314,9 @@ export async function decideParkedAgent(deps, req, match) {
313
314
  if (isErr) {
314
315
  const disp = await rollback("execute_rejected");
315
316
  const content = receipt.content;
316
- const receiptError = receipt.details?.error;
317
- if (receiptError === "parked_resume.startup_failed") {
317
+ const receiptDetails = receipt.details;
318
+ if (receiptDetails?.error === "parked_resume.startup_failed") {
319
+ const cause = readClosedWordSeat(receiptDetails, "cause");
318
320
  return {
319
321
  status: 422,
320
322
  body: {
@@ -322,6 +324,7 @@ export async function decideParkedAgent(deps, req, match) {
322
324
  errorCode: "parked_resume.startup_failed",
323
325
  taskId: match.handle,
324
326
  rollback: disp,
327
+ ...(cause !== undefined ? { cause } : {}),
325
328
  },
326
329
  };
327
330
  }