@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +6 -6
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
@@ -1,8 +1,8 @@
1
1
  import { screenedGate } from "../../trace/project.js";
2
- import { hostDecisionFacts, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
2
+ import { hostDecisionFacts, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, standingAnswerDurablyRecordable } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, carriesDeploymentListQuestion, standingAnswerDurablyRecordable } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -17,6 +17,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
+ import { deploymentListAsks } from "../../approval.js";
20
21
  import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
22
  import { sendResumeOutcome } from "../route-ctx.js";
22
23
  import { assertNever } from "../route-ctx.js";
@@ -640,13 +641,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
640
641
  const body = (await readJson(req));
641
642
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
642
643
  if (!decision) {
643
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }");
644
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }", retiredDecisionKeyFacts(body)?.extra);
644
645
  return;
645
646
  }
646
647
  {
647
648
  const keyIssue = decisionBodyKeyIssue("approval-decide", body);
648
649
  if (keyIssue) {
649
- sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
650
+ const retired = retiredDecisionKeyFacts(body);
651
+ sendError(res, 400, "request.body_shape", retired?.error ?? keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys, ...retired?.extra });
650
652
  return;
651
653
  }
652
654
  }
@@ -819,7 +821,8 @@ function redactPendingDisclosures(row) {
819
821
  const next = redactRiskDescriptor(rd);
820
822
  return next === rd ? row : { ...row, riskDescriptor: next };
821
823
  }
822
- function projectPendingForWire(row, governance) {
824
+ function projectPendingForWire(row, deployment) {
825
+ const governance = deployment;
823
826
  const redacted = redactPendingDisclosures(row);
824
827
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
825
828
  const rememberRefused = !standingAnswerDurablyRecordable({
@@ -828,6 +831,7 @@ function projectPendingForWire(row, governance) {
828
831
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
829
832
  grantBinding: grantBindingOf(row),
830
833
  classes: row.classes,
834
+ deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
831
835
  });
832
836
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
833
837
  }
@@ -1,4 +1,4 @@
1
- import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
1
+ import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { modelImageInput, isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { projectSealedKeyCapability } from "../../sealed-key.js";
@@ -23,7 +23,7 @@ import { workflowModelAllowlistFor } from "../../task-workflow.js";
23
23
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
24
24
  import * as facts from "../../capability-facts.js";
25
25
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
26
- import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
26
+ import { coercePermissionModeStrict, PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
27
27
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
28
28
  function routePairingOf(deps, name, m) {
29
29
  if (modelKeyPoisonOf(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {}) !== undefined)
@@ -60,7 +60,7 @@ export async function handleCapabilities(req, res, match, ctx) {
60
60
  const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
61
61
  const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
62
62
  if (requestedModeRaw !== null && requestedMode === undefined) {
63
- sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
63
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${PERMISSION_MODE_WORDS}`);
64
64
  return;
65
65
  }
66
66
  const entitlementSource = centerEntitlementSourceWired(deps.config);
@@ -160,6 +160,8 @@ export async function handleCapabilities(req, res, match, ctx) {
160
160
  streamApproval: facts.streamApproval(deps).available,
161
161
  approvalDecisionNote: true,
162
162
  approvalDecidedBy: true,
163
+ permissionModes: [...PERMISSION_MODES],
164
+ sessionReadRedaction: true,
163
165
  sideQuery: true,
164
166
  agentRoster: facts.agentRoster(deps).available,
165
167
  retention: retentionAdvertisement(deps.config),
@@ -2,7 +2,7 @@ import { sendError, sendJson } from "../send.js";
2
2
  import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
- import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
5
+ import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, isDeviceAbsolutePath, sameDeviceOwner, } from "../../device-store.js";
6
6
  import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
7
7
  import { deviceOwnerDomainOf } from "../device-placement.js";
8
8
  import { echoKeyName } from "../../request-key-closure.js";
@@ -109,8 +109,8 @@ function parseEnrollBody(res, body) {
109
109
  return null;
110
110
  }
111
111
  }
112
- if (workspaceRoot.length === 0) {
113
- sendError(res, 400, "request.field_invalid", "workspaceRoot is required and must be non-empty (it is the device-side root every instruction resolves against)");
112
+ if (!isDeviceAbsolutePath(workspaceRoot)) {
113
+ sendError(res, 400, "request.field_invalid", "workspaceRoot must be an absolute POSIX path (it is the device-side root every instruction resolves against)");
114
114
  return null;
115
115
  }
116
116
  return { token: token, pubkey: pubkey, platformOs: os, platformArch: platformArch, displayName: displayName, workspaceRoot: workspaceRoot };
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
114
114
  baseRev: string;
115
115
  origin: {
116
116
  at: number;
117
- cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
117
+ cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
118
118
  taint: "external";
119
119
  };
120
120
  };
@@ -4,10 +4,10 @@ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sen
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import * as facts from "../../capability-facts.js";
6
6
  import { createLogger } from "../../observability/logger.js";
7
- import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
7
+ import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
8
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
9
9
  import { HttpError } from "../../security.js";
10
- import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
10
+ import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
11
11
  import { PERSISTED_RULE_MATCHES, isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
12
12
  const logger = createLogger();
13
13
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
@@ -26,7 +26,7 @@ const CcImportPrepareBodySchema = z
26
26
  .object({
27
27
  layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
28
28
  path: z.string().min(1).max(1024),
29
- root: z.string().min(1).max(1024),
29
+ root: z.string().max(1024),
30
30
  content: z.string().max(MAX_LAYER_CONTENT_CHARS),
31
31
  })
32
32
  .strict())
@@ -34,6 +34,9 @@ const CcImportPrepareBodySchema = z
34
34
  .max(MAX_LAYERS),
35
35
  })
36
36
  .strict();
37
+ function ccImportRootRefused(layer) {
38
+ return !isValidDurableScope({ kind: "project", root: layer.root });
39
+ }
37
40
  const TICKET_CONTROL_CHARS_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
38
41
  const TicketStringSchema = z.string().min(1).max(190).refine((t) => !TICKET_CONTROL_CHARS_RE.test(t));
39
42
  const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
@@ -170,6 +173,10 @@ export async function handleRules(req, res, match, ctx) {
170
173
  sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
171
174
  return;
172
175
  }
176
+ if (parsed.data.layers.some(ccImportRootRefused)) {
177
+ sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
178
+ return;
179
+ }
173
180
  const prepared = await lane.prepareImport(principal, parsed.data.layers);
174
181
  if (!prepared.ok) {
175
182
  sendError(res, 413, "request.payload_too_large", "too many allow entries across the submitted settings layers");
@@ -231,7 +238,7 @@ async function handleRuleList(res, search, caller, lane, config) {
231
238
  const scopeRaw = q.get("scope");
232
239
  const scopeFilter = scopeRaw === null ? undefined : parseRuleScope(scopeRaw);
233
240
  if (scopeRaw !== null && scopeFilter === undefined) {
234
- sendError(res, 400, "request.query_invalid", "scope must be 'global' or 'project:<root>' with a non-empty root");
241
+ sendError(res, 400, "request.query_invalid", RULE_SCOPE_REFUSAL_SENTENCE);
235
242
  return;
236
243
  }
237
244
  const cursorRaw = q.get("cursor");
@@ -305,7 +312,7 @@ async function handleRuleWrite(res, rawBody, caller, lane, config) {
305
312
  }
306
313
  const scope = parseRuleScope(parsed.data.scope);
307
314
  if (scope === undefined) {
308
- sendError(res, 400, "request.body_shape", "scope must be 'global' or 'project:<root>' with a non-empty root");
315
+ sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
309
316
  return;
310
317
  }
311
318
  const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
@@ -354,7 +361,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, config) {
354
361
  }
355
362
  const scope = parseRuleScope(parsed.data.scope);
356
363
  if (scope === undefined) {
357
- sendError(res, 400, "request.body_shape", "scope must be 'global' or 'project:<root>' with a non-empty root");
364
+ sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
358
365
  return;
359
366
  }
360
367
  const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
@@ -1,7 +1,7 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { z } from "zod";
4
- import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
4
+ import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
5
5
  import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
@@ -15,14 +15,14 @@ import { scopedIdempotencyKey } from "../idempotency.js";
15
15
  import { streamSseLog } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
18
- import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
18
+ import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
19
19
  import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
20
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
21
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
22
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
23
23
  import { readTurnActivityMs } from "../../turn-activity.js";
24
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
- import { userInputLaneOf } from "../../user-input-ledger.js";
25
+ import { acceptedInputKindOf } from "../../user-input-ledger.js";
26
26
  import { INJECTION_TIERS } from "../../trace/injection-tier.js";
27
27
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
28
28
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
@@ -150,8 +150,9 @@ function sendSteerReplayRefusal(res, refusal) {
150
150
  async function acceptLiveUserInput(deps, res, at) {
151
151
  if (!deps.userInputLedger)
152
152
  return undefined;
153
+ const input = acceptedInputKindOf(at.tier);
153
154
  try {
154
- return await deps.userInputLedger.accept({ input: userInputLaneOf(at.tier), messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
155
+ return await deps.userInputLedger.accept({ input, messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
155
156
  }
156
157
  catch (e) {
157
158
  deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
@@ -1372,6 +1373,11 @@ export async function handleRunVerbs(req, res, match, ctx) {
1372
1373
  }
1373
1374
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1374
1375
  if (!parsedBody.success) {
1376
+ const retired = retiredDecisionKeyFacts(rawBody);
1377
+ if (retired !== undefined) {
1378
+ sendError(res, 400, "request.body_shape", retired.error, retired.extra);
1379
+ return;
1380
+ }
1375
1381
  sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1376
1382
  return;
1377
1383
  }
@@ -1517,7 +1523,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1517
1523
  sendTerminal(row0);
1518
1524
  return;
1519
1525
  }
1520
- if (policyCannotAllow(decidedBy, body.decision === "approve", storedCardDemandsAPerson(row0.cardJson))) {
1526
+ if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
1521
1527
  sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
1522
1528
  return;
1523
1529
  }
@@ -1,7 +1,7 @@
1
- import { materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
1
+ import { SessionError, 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
- import { windowMessages, truncateMessageBlobs } from "../../audit.js";
4
+ import { windowMessages, truncateMessageBlobs, redactSessionViewForRead } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
@@ -596,9 +596,13 @@ export async function handleSessions(req, res, match, ctx) {
596
596
  sendError(res, 501, "capability.session_store_required", "the session audit view requires a durable session store (DB_BACKEND=mysql|pg|local)");
597
597
  return;
598
598
  }
599
- const audit = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
600
- if (audit === undefined)
599
+ const owned = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
600
+ if (owned === undefined)
601
601
  return;
602
+ if (!Array.isArray(owned.messages)) {
603
+ throw new SessionError("corrupt_session", `session ${match.params.sessionId} is unreadable: its archive projection carries "messages" as ${owned.messages === null ? "null" : typeof owned.messages}, not an array`);
604
+ }
605
+ const ownedMessages = owned.messages;
602
606
  const q = new URLSearchParams(match.search);
603
607
  const qi = (k) => {
604
608
  const v = q.get(k);
@@ -609,12 +613,12 @@ export async function handleSessions(req, res, match, ctx) {
609
613
  };
610
614
  const msgIndex = qi("message");
611
615
  if (msgIndex !== undefined) {
612
- const messages = audit.messages;
613
- if (msgIndex >= messages.length) {
614
- sendError(res, 404, "not_found.message_index", "message index out of range", { total: messages.length });
616
+ if (msgIndex >= ownedMessages.length) {
617
+ sendError(res, 404, "not_found.message_index", "message index out of range", { total: ownedMessages.length });
615
618
  return;
616
619
  }
617
- sendJson(res, 200, { sessionId: audit.sessionId, floorEntryId: audit.floorEntryId, index: msgIndex, message: messages[msgIndex] });
620
+ const one = redactSessionViewForRead({ ...owned, messages: [ownedMessages[msgIndex]] });
621
+ sendJson(res, 200, { sessionId: owned.sessionId, floorEntryId: owned.floorEntryId, index: msgIndex, message: one.messages[0] });
618
622
  return;
619
623
  }
620
624
  const tail = qi("tail");
@@ -631,19 +635,19 @@ export async function handleSessions(req, res, match, ctx) {
631
635
  }
632
636
  const undrainedKey = undrained !== undefined ? { userInputUndrained: undrained } : {};
633
637
  if (tail === undefined && before === undefined && blobCap === undefined) {
634
- sendJson(res, 200, { ...audit, ...undrainedKey });
638
+ sendJson(res, 200, { ...redactSessionViewForRead(owned), ...undrainedKey });
635
639
  return;
636
640
  }
637
- let messages = audit.messages;
641
+ let selected = ownedMessages;
638
642
  let window;
639
643
  if (tail !== undefined || before !== undefined) {
640
- const w = windowMessages(messages, { ...(tail !== undefined ? { tail } : {}), ...(before !== undefined ? { before } : {}), ...(qi("limit") !== undefined ? { limit: qi("limit") } : {}) });
641
- messages = w.messages;
644
+ const w = windowMessages(ownedMessages, { ...(tail !== undefined ? { tail } : {}), ...(before !== undefined ? { before } : {}), ...(qi("limit") !== undefined ? { limit: qi("limit") } : {}) });
645
+ selected = w.messages;
642
646
  window = w.window;
643
647
  }
644
- if (blobCap !== undefined && blobCap > 0)
645
- messages = truncateMessageBlobs(messages, blobCap);
646
- sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}), ...undrainedKey });
648
+ const view = redactSessionViewForRead({ ...owned, messages: selected });
649
+ const messages = blobCap !== undefined && blobCap > 0 ? truncateMessageBlobs(view.messages, blobCap) : view.messages;
650
+ sendJson(res, 200, { ...view, messages, ...(window ? { window } : {}), ...undrainedKey });
647
651
  return;
648
652
  }
649
653
  }
@@ -756,7 +760,7 @@ export async function handleSessions(req, res, match, ctx) {
756
760
  return;
757
761
  }
758
762
  if (owner === null) {
759
- sendError(res, 409, "conflict.session_ownerless", "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless");
763
+ sendError(res, 409, "conflict.session_ownerless", "this session has no recorded owner, so its rules can be neither read nor changed through this route; rules already stored for it keep applying to the runs of the principal they were stored under. Ways out: on the local backend, during the upgrade window, the first task submitted with a principal on a session written by an earlier version claims it for that principal (this route then serves its owner); or delete the session (DELETE /v1/sessions/:id), which removes its rules with it");
760
764
  return;
761
765
  }
762
766
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -6,8 +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, ApprovalReach, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
- import type { SettingsPermissionMode } from "../task-settings.js";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec } from "@sema-agent/core";
11
10
  import type { wiringManifestFaceEventData } from "../trace/project.js";
12
11
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
13
12
  export interface TaskRequestBody {
@@ -17,7 +16,9 @@ export interface TaskRequestBody {
17
16
  * table — e.g. "default"/"code-review"/"scan"/"discuss"). An INTENT bounded by the deployment's own table AND,
18
17
  * on a governed worker, by the principal's center-resolved allowlist: an unknown name is a typed 400
19
18
  * `scenario_unknown`, a known-but-not-allowed one a typed 400 `scenario_not_allowed`; absent ⇒ the ruling's
20
- * assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`).
19
+ * assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`). A name
20
+ * on that default chain this deployment does not have is NEVER folded to `default`: an assigned one is a typed 503
21
+ * `state.scenario_assignment_unknown`, a dangling `DEFAULT_SCENARIO` refuses to boot.
21
22
  * ⚠️ scenario-SPECIFIC keys (`repo`/`lenses`/`rounds`, read through `ScenarioRequest`'s index signature) are
22
23
  * deliberately NOT declared here — they belong to one scenario each, not to the shared submit surface. */
23
24
  scenario?: string;
@@ -333,18 +334,16 @@ export interface TaskRequestBody {
333
334
  toolMaterializeStrategy?: "static" | "swap";
334
335
  /** R4 (CC parity): the LIGHT top-level per-turn permission-mode intent (CC `permissionMode`). The shell sends
335
336
  * the RAW mode (axis-agnostic, no client-side interpretation); the SERVICE interprets it tighten-only vs the
336
- * deployment baseline. Post-[ref] all FIVE modes are honored as gate-SHAPE choices (`plan` ⇒ read-only hands +
337
- * `present_plan`; `acceptEdits`/`bypassPermissions`/`auto` select how much the mode-derived fs-write ask gate
338
- * asks — they can never subtract from deployment/operator policy, tightenTaskSpec deny-wins; the old "LOOSEN →
339
- * coerced to default" note was pre-[ref] doc-rot). Folds onto the SAME tighten-only governance as
340
- * `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path.
337
+ * deployment baseline. All SIX core modes are honored (`plan` ⇒ read-only hands + `present_plan`;
338
+ * `acceptEdits` selects how much the mode-derived fs-write ask gate asks; `bypassPermissions`/`auto`/`dontAsk` ride the
339
+ * engine's own mode table verbatim — `dontAsk` refuses every ask by the mode, zero cards, the gate records
340
+ * `deniedBy:"mode"`) — they can never subtract from deployment/operator policy, tightenTaskSpec deny-wins). Folds onto the SAME
341
+ * tighten-only governance as `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path.
341
342
  * [ref]-②: CLOSED enum on the wire — an unknown word 400s at submit (`request.field_invalid`, sibling of
342
343
  * promptProfile) instead of silently coercing to `default`; resume replay of a stored body stays lenient
343
- * (coercePermissionMode). */
344
- /** 🔴 型**从值表派生**(S-496 U1 / [ref]):闭集的属主是 `task-settings.ts` 的
345
- * `SETTINGS_PERMISSION_MODES`(受理腿 `isSettingsPermissionMode` 与两句 400 文案同读那一份)。
346
- * 改前这里是第 6 份手抄的五词联合 —— 加/退一个模式时 wire 型面与受理腿会各走各的。 */
347
- permissionMode?: SettingsPermissionMode;
344
+ * (coercePermissionMode).
345
+ * 🔴 型与闭集的属主是 core 的 `PermissionMode` / `PERMISSION_MODES`(受理腿 `isPermissionMode` 与三句 400 文案同读那一份)—— 本仓不持词表。 */
346
+ permissionMode?: PermissionMode;
348
347
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
349
348
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the [ref] memory ENGINE materializes/
350
349
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
@@ -6,7 +6,13 @@
6
6
  * one-time seed and the P3 atomic-publish pipeline — kept side-effect-free so it is unit-tested without a DB.
7
7
  */
8
8
  import type { ImageIndexUpsert, ImageNestedBuildMode, ImageStatus } from "../plugins/store-contracts.js";
9
- /** Project the manifest's `nestedBuild` (string posture OR bool) → the enum (center contract). */
9
+ /**
10
+ * Project the manifest's `nestedBuild` (string posture OR bool) → the enum (center contract).
11
+ *
12
+ * 声明型 = 闭集词 | `true`(老写法的裸布尔,含义 = `rootless-buildkit`)| `false` / 缺席(= `none`)。**其余一律抛** —— 修前「未知的非空词」
13
+ * 被上调成 `rootless-buildkit`(一条没人声明过的 nested-build 能力写进 index),运行期的非声明型真值(数字 / 对象 …)走 `v ? … : …` 同样被上调。
14
+ * 抛出的句子沿用该面既有的拒法:`done` 帧的 auto-register 把它收成「built-but-unregistered」终局 FAILED,错误句带这一句(见 `http/routes/images.ts`)。
15
+ */
10
16
  export declare function nestedBuildModeOf(v: string | boolean | undefined): ImageNestedBuildMode;
11
17
  /** The fields of build.sh's `sandbox-manifest.json` this projection reads (a superset is fine; extras ignored). */
12
18
  export interface BuildManifest {
@@ -1,18 +1,19 @@
1
- const NESTED_BUILD_MODES = [
2
- "none",
3
- "docker-cli-only",
4
- "rootless-buildkit",
5
- "privileged-dind",
6
- ];
1
+ const NESTED_BUILD_MODE_WORDS = {
2
+ none: true,
3
+ "docker-cli-only": true,
4
+ "rootless-buildkit": true,
5
+ "privileged-dind": true,
6
+ };
7
+ const isNestedBuildMode = (v) => Object.prototype.hasOwnProperty.call(NESTED_BUILD_MODE_WORDS, v);
7
8
  export function nestedBuildModeOf(v) {
8
- if (typeof v === "string") {
9
- return NESTED_BUILD_MODES.includes(v)
10
- ? v
11
- : v
12
- ? "rootless-buildkit"
13
- : "none";
14
- }
15
- return v ? "rootless-buildkit" : "none";
9
+ if (v === undefined || v === false)
10
+ return "none";
11
+ if (v === true)
12
+ return "rootless-buildkit";
13
+ if (typeof v === "string" && isNestedBuildMode(v))
14
+ return v;
15
+ const got = typeof v === "string" ? `the string ${JSON.stringify(v.slice(0, 64))}` : v === null ? "null" : Array.isArray(v) ? "an array" : `type ${typeof v}`;
16
+ throw new Error(`manifest capabilities.nestedBuild must be one of ${Object.keys(NESTED_BUILD_MODE_WORDS).join(" | ")} (or a bare boolean); got ${got}`);
16
17
  }
17
18
  export function shmToMB(v) {
18
19
  if (!v)
@@ -32,11 +33,12 @@ export function profileFromManifest(m) {
32
33
  export function manifestToIndexEntry(m, opts) {
33
34
  const profile = profileFromManifest(m);
34
35
  const tag = (m.tag ?? "").includes(":") ? (m.tag ?? "").split(":")[1] : (m.tag ?? "");
36
+ const nestedBuildMode = nestedBuildModeOf(m.capabilities?.nestedBuild);
35
37
  const capabilities = {
36
38
  browser: !!m.capabilities?.browser,
37
39
  db: !!m.capabilities?.db,
38
- nestedBuild: !!m.capabilities?.nestedBuild,
39
- nestedBuildMode: nestedBuildModeOf(m.capabilities?.nestedBuild),
40
+ nestedBuild: nestedBuildMode !== "none",
41
+ nestedBuildMode,
40
42
  };
41
43
  const pc = m.capabilities?.podContract;
42
44
  const devShmMB = shmToMB(pc?.devShm);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * **机读位的值** —— 通告 `detail` 里的规则文本(`permission.allowed_tool_not_granted` 的 `detail.entries[].entry`、`permission.rule_row_dropped` 的
3
+ * `detail.rows[].rule`)在会话流行与运维日志行上一律是这一只:条目的脱敏原值。显示形不进机读位(壳若再加引号就是两层)。
4
+ */
5
+ export declare function redactListEntry(entry: string): string;
6
+ /**
7
+ * **人读句里条目的唯一写法** —— 本仓把规则条目 / 工具名原串写进任何一句给人读的话(拒句、起服拒启行)一律调这一只:引擎的条目写者
8
+ * `renderListEntry`(core 7.46.0 #1428 包根导出:JSON 串形带引号,看不见 / 印成空白的字符写成 `\uXXXX`,超长截断)—— 与引擎自己的通告句同一个渲染;
9
+ * 判定拿原串。
10
+ * 🔴 凭据先认、再渲染:渲染把看不见的字符写成 ASCII 转义,之后的脱敏器就认不出夹着零宽字符的凭据 ⇒ 原串上认出凭据时渲染它的脱敏值;
11
+ * 没有凭据的条目渲染**原串**(格式字符留给渲染器写成 `\uXXXX` —— 人要看得见它,不能先被脱敏器剥掉)。
12
+ */
13
+ export declare function displayListEntry(entry: string): string;
14
+ //# sourceMappingURL=list-entry-display.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { renderListEntry } from "@sema-agent/core";
2
+ import { redactSecretsWithArms } from "./trace/redact.js";
3
+ function scanListEntry(entry) {
4
+ const scan = redactSecretsWithArms(entry);
5
+ return { redacted: scan.text, credential: scan.arms.size > 0 };
6
+ }
7
+ export function redactListEntry(entry) {
8
+ return scanListEntry(entry).redacted;
9
+ }
10
+ export function displayListEntry(entry) {
11
+ const scan = scanListEntry(entry);
12
+ return renderListEntry(scan.credential ? scan.redacted : entry);
13
+ }
14
+ //# sourceMappingURL=list-entry-display.js.map
@@ -34,27 +34,22 @@
34
34
  * 路径不上 wire,只进这一条运维 warn —— 它是**日志行**不是 wire 错误体,所以铸点跟读法属主住在一起(不住 `http/`:`http/` 全树门
35
35
  * `test/error-code-key-gate.test.ts` 咬的是错误体里的 `code:` 键位,日志字段名不该借 wire 层的目录去过它)。
36
36
  */
37
+ import { type ScopeHomeForm, type StoreFormRefusalCode } from "@sema-agent/core";
37
38
  import type { Logger } from "./observability/logger.js";
38
- /** core 的两码(`dist/core/memory-engine/layout.js` `TARGET_OUTSIDE_STORE` / `SCOPE_HOME_NOT_DIRECTORY`;core 不在包根导出它们,这两个常量是它们在本仓的唯一写点)。 */
39
- export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
40
- export declare const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
41
- /** 店形拒族(闭集;core 加第三码 ⇒ 它落未分类 500 + 运维 warn,由人裁归不归这一族)。 */
42
- export declare const STORE_FORM_REFUSAL_CODES: readonly ["memory.target_outside_store", "memory.scope_home_not_directory"];
43
- export type StoreFormRefusalCode = (typeof STORE_FORM_REFUSAL_CODES)[number];
44
39
  /** 一次店形拒的读数(纯数据)。`path` / `form` 取自 core 的 `detail`(读不出 ⇒ `undefined`,不猜、不从句子里抠)。 */
45
- export interface StoreFormRefusal {
40
+ export interface StoreFormRefusalReading {
46
41
  readonly code: StoreFormRefusalCode;
47
42
  /** core 原句(以码领头;7.44.0 起不含宿主路径)。 */
48
43
  readonly message: string;
49
44
  /** `detail.path`:落点 / 被改指的根 / 不是目录的 scope home —— **只给运维行**。 */
50
45
  readonly path: string | undefined;
51
- /** `detail.form`(只在 home 那一码上):原样字符串,不镜像 core 的闭集(本仓只把它写进运维行,不按它分支)。 */
52
- readonly form: string | undefined;
46
+ /** `detail.form`(只在 home 那一码上),经 core 闭集读;本仓只把它写进运维行,不按它分支。 */
47
+ readonly form: ScopeHomeForm | undefined;
53
48
  }
54
- /** 抛出面:`err` 是店形拒 ⇒ 读数;否则 `undefined`。形 = core 的 `Error` + `.code` + `.detail`。 */
55
- export declare function storeFormRefusalOf(err: unknown): StoreFormRefusal | undefined;
49
+ /** 抛出面:`err` 是店形拒(core `StoreFormRefusal` 的形:码在族里)⇒ 读数;否则 `undefined`。`detail` 读不全不改判(码定族,`detail` 只供运维行)。 */
50
+ export declare function storeFormRefusalOf(err: unknown): StoreFormRefusalReading | undefined;
56
51
  /** 两受众共用的运维行(一处铸点):码 + core `detail` 里的宿主路径与 home 形。`logger` 缺席(只有不接 logger 的测试装配)⇒ 不落。 */
57
- export declare function logStoreFormRefusal(logger: Pick<Logger, "warn"> | undefined, refusal: StoreFormRefusal): void;
52
+ export declare function logStoreFormRefusal(logger: Pick<Logger, "warn"> | undefined, refusal: StoreFormRefusalReading): void;
58
53
  /** 报告面:harvest `incident` 是这一族拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成店形拒)。 */
59
54
  export declare function storeFormRefusalInIncident(incident: {
60
55
  readonly kind: string;
@@ -1,7 +1,6 @@
1
- export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
2
- export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
3
- export const STORE_FORM_REFUSAL_CODES = [TARGET_OUTSIDE_STORE, SCOPE_HOME_NOT_DIRECTORY];
4
- const isStoreFormRefusalCode = (code) => typeof code === "string" && STORE_FORM_REFUSAL_CODES.includes(code);
1
+ import { SCOPE_HOME_FORMS, STORE_FORM_REFUSAL_CODES } from "@sema-agent/core";
2
+ const isStoreFormRefusalCode = (code) => STORE_FORM_REFUSAL_CODES.includes(code);
3
+ const isScopeHomeForm = (form) => SCOPE_HOME_FORMS.includes(form);
5
4
  export function storeFormRefusalOf(err) {
6
5
  if (!(err instanceof Error))
7
6
  return undefined;
@@ -9,7 +8,7 @@ export function storeFormRefusalOf(err) {
9
8
  if (!isStoreFormRefusalCode(code))
10
9
  return undefined;
11
10
  const d = typeof detail === "object" && detail !== null ? detail : {};
12
- return { code, message: err.message, path: typeof d.path === "string" ? d.path : undefined, form: typeof d.form === "string" ? d.form : undefined };
11
+ return { code, message: err.message, path: typeof d.path === "string" ? d.path : undefined, form: isScopeHomeForm(d.form) ? d.form : undefined };
13
12
  }
14
13
  export function logStoreFormRefusal(logger, refusal) {
15
14
  logger?.warn("memory_store_form_refused", {
@@ -14,9 +14,10 @@
14
14
  * · 会话规则写口(`session-policy-wire.ts`)与 SQL 会话规则店的写口(`assertSessionRuleNamesReachable`);
15
15
  * · 引擎 allow 层的条目表:请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT`、技能的两只清单 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代
16
16
  * `agents[].skills[]` 两处受理口)—— 经 `rule-entry-list.ts` 的 `emptyToolSegmentEntryOf`(本仓这一问;退役名 / 铸不出的判词那一半由引擎 `compileAllowList` 给)。
17
- * **不调它的面**(如实,未接):`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)、子代的工具名表
18
- * `agents[].allowTools` / `denyTools`(`spec-fields.ts` 只判形与条数)。在这两处写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中。把这些面接上判官 = 另一件事
19
- * (本版不做,也不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
17
+ * **不调它的面**:`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)—— 如实未接,
18
+ * 在那里写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中;把它接上判官 = 另一件事(不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
19
+ * 子代的工具名表 `agents[].allowTools` / `denyTools` 不是本文件的面:core 7.45.0 #1377 / #1385 起名字归引擎在开跑前一只判(三来源同判,`spec-fields.ts`
20
+ * 受理口只判形与条数),本仓不对它另做对照。
20
21
  * **精确名的工具段归一不在这里判**:包根没有导出工具段的铸法,不手抄 —— 精确名的工具段按字面收,本仓每张表上都不判它。
21
22
  *
22
23
  * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表与技能清单同读)。
@@ -1,5 +1,5 @@
1
1
  import { PROTOCOL_TABLE, isNamespacedCoveringRuleName, judgeRuleToolName, parsePermissionRule, protocolOf } from "@sema-agent/core";
2
- import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
2
+ import { displayListEntry } from "./list-entry-display.js";
3
3
  export function ruleEntryToolName(entry) {
4
4
  return parsePermissionRule(entry).toolName;
5
5
  }
@@ -11,10 +11,6 @@ export function unmatchableRuleNameOf(name) {
11
11
  const ns = emptyToolSegmentOf(name);
12
12
  return ns === undefined ? undefined : { kind: "empty_tool_segment", ns };
13
13
  }
14
- function presentName(name) {
15
- const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
16
- return clean.length <= 160 ? clean : `${clean.slice(0, 160)}…(${name.length} chars)`;
17
- }
18
14
  export function describeUnmatchableRuleName(v, name) {
19
15
  switch (v.kind) {
20
16
  case "retired":
@@ -22,7 +18,7 @@ export function describeUnmatchableRuleName(v, name) {
22
18
  case "unmintable_segment":
23
19
  return v.guidance;
24
20
  case "unprefixed_namespace":
25
- return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentName(name)}`).join(" or ")}`;
21
+ return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => displayListEntry(`${e.prefix}${name}`)).join(" or ")}`;
26
22
  case "empty_tool_segment":
27
23
  return describeEmptyToolSegment(v.ns);
28
24
  default: {
@@ -356,7 +356,7 @@ export declare const FAIL_OPEN_TAGS: {
356
356
  };
357
357
  readonly "server.session-repo.corrupt-record-skipped": {
358
358
  readonly cls: "F";
359
- readonly note: "S-990(`plugins/store-backend.ts` 的 `FileSessionRepo` 座,local 车道的会话存档):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。";
359
+ readonly note: "S-990(`plugins/local-session-store.ts` `createLocalSessionRepo` 的 `FileSessionRepo` 座,local 车道的会话存档;`LocalSessionStore` 与会话视图口 `audit.ts` 共用,S-879):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。";
360
360
  };
361
361
  readonly "server.workflow-journal.corrupt-record-skipped": {
362
362
  readonly cls: "F";
@@ -390,6 +390,14 @@ export declare const FAIL_OPEN_TAGS: {
390
390
  readonly cls: "F";
391
391
  readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";
392
392
  };
393
+ readonly "server.snapshot-bucket.presence-unproven": {
394
+ readonly cls: "F";
395
+ readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
396
+ };
397
+ readonly "server.config-lkg.landed-path-unresolved": {
398
+ readonly cls: "P-DEBT";
399
+ readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
400
+ };
393
401
  };
394
402
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
395
403
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;