@sema-agent/server 7.125.1 → 7.127.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 (275) hide show
  1. package/MIGRATION.md +229 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +346 -145
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +2 -1
  10. package/deploy/sema-up/chart/values.yaml +22 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +20 -13
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  18. package/deploy/sema-up/lib/parse-machines.py +2 -2
  19. package/deploy/sema-up/lib/shell-access.sh +1 -1
  20. package/deploy/sema-up/preflight.sh +4 -4
  21. package/deploy/sema-up/sema-up.sh +33 -33
  22. package/deploy/sema-up/smoke.sh +3 -3
  23. package/deploy/sema-up/stack/compose.yaml +1 -1
  24. package/deploy/sema-up/stack/sema-stack-cluster.yaml +26 -3
  25. package/dist/approval-card.d.ts +20 -4
  26. package/dist/approval-content-kind.d.ts +38 -22
  27. package/dist/approval-content-kind.js +20 -4
  28. package/dist/audit.js +3 -0
  29. package/dist/bench/s1/arms.d.ts +9 -37
  30. package/dist/bench/s1/arms.js +4 -61
  31. package/dist/bench/s1/live-deps.d.ts +4 -10
  32. package/dist/bench/s1/live-deps.js +1 -222
  33. package/dist/bench/s1/oracle.d.ts +1 -1
  34. package/dist/bench/s1/reviewer.d.ts +1 -1
  35. package/dist/bench/s1/row.d.ts +5 -7
  36. package/dist/bench/s1/row.js +0 -2
  37. package/dist/bench/s1/run-firm.d.ts +2 -2
  38. package/dist/bench/s1/run-firm.js +1 -4
  39. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  40. package/dist/bench/s1/runner-ctx.js +3 -7
  41. package/dist/bench/s1/tasks.d.ts +9 -46
  42. package/dist/bench/s1/tasks.js +4 -63
  43. package/dist/boot/execution-env.d.ts +38 -26
  44. package/dist/boot/execution-env.js +87 -41
  45. package/dist/boot/leader.d.ts +2 -1
  46. package/dist/boot/memory-boundary.d.ts +1 -1
  47. package/dist/boot/memory-consolidation.d.ts +37 -3
  48. package/dist/boot/memory-consolidation.js +18 -2
  49. package/dist/boot/parked-revive-gate.d.ts +0 -3
  50. package/dist/boot/parked-revive-gate.js +2 -2
  51. package/dist/boot/resolve-spec.d.ts +0 -2
  52. package/dist/boot/resolve-spec.js +12 -17
  53. package/dist/boot/retention-lane.d.ts +12 -1
  54. package/dist/boot/retention-lane.js +2 -0
  55. package/dist/boot/runner-deps.d.ts +7 -6
  56. package/dist/boot/runner-deps.js +5 -16
  57. package/dist/boot/session-faces.d.ts +7 -2
  58. package/dist/boot/session-faces.js +37 -1
  59. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  60. package/dist/boot/stage-03-store-backend.js +2 -2
  61. package/dist/boot/stage-04-stores.d.ts +1 -0
  62. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  63. package/dist/boot/stage-05-execution-env.js +7 -4
  64. package/dist/boot/stage-06-runners.d.ts +2 -3
  65. package/dist/boot/stage-06-runners.js +13 -9
  66. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  67. package/dist/boot/stage-07-capability-layer.js +3 -4
  68. package/dist/boot/stage-08-reapers.d.ts +2 -3
  69. package/dist/boot/stage-08-reapers.js +2 -2
  70. package/dist/boot/stage-09-leader.d.ts +2 -3
  71. package/dist/boot/stage-09-leader.js +2 -1
  72. package/dist/boot/stage-10-http-server.d.ts +2 -3
  73. package/dist/boot/stage-10-http-server.js +2 -2
  74. package/dist/boot/stores.d.ts +1 -0
  75. package/dist/boot/stores.js +4 -3
  76. package/dist/boot/web-admin.d.ts +5 -0
  77. package/dist/boot/web-admin.js +28 -14
  78. package/dist/brain.d.ts +2 -2
  79. package/dist/brain.js +1 -1
  80. package/dist/capabilities/builtin-tools.d.ts +2 -1
  81. package/dist/capabilities/composite-members.d.ts +1 -1
  82. package/dist/capabilities/composite-members.js +1 -1
  83. package/dist/capabilities/hands-lane.d.ts +9 -6
  84. package/dist/capabilities/hands-lane.js +2 -2
  85. package/dist/capabilities/memory-notice.d.ts +27 -22
  86. package/dist/capabilities/memory-notice.js +15 -17
  87. package/dist/capabilities/scenarios.d.ts +9 -14
  88. package/dist/capabilities/scenarios.js +5 -50
  89. package/dist/capability-facts.d.ts +22 -11
  90. package/dist/capability-facts.js +13 -6
  91. package/dist/config-catalog.js +14 -5
  92. package/dist/config-provider.d.ts +5 -1
  93. package/dist/config-provider.js +23 -4
  94. package/dist/config-types.d.ts +19 -2
  95. package/dist/config.js +41 -6
  96. package/dist/deployment-governance.d.ts +19 -21
  97. package/dist/deployment-governance.js +7 -8
  98. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  99. package/dist/env-name-allowlist-knobs.js +1 -0
  100. package/dist/execution-lane-caps.d.ts +12 -0
  101. package/dist/execution-lane-caps.js +18 -4
  102. package/dist/fleet/subagent-tail-bus.js +15 -5
  103. package/dist/hooks/hook-runner.d.ts +84 -30
  104. package/dist/hooks/hook-runner.js +74 -41
  105. package/dist/http/admission.js +17 -10
  106. package/dist/http/durable-run-leg.d.ts +5 -4
  107. package/dist/http/durable-run-leg.js +2 -4
  108. package/dist/http/idempotency.d.ts +28 -6
  109. package/dist/http/idempotency.js +10 -2
  110. package/dist/http/principal-gate.d.ts +14 -0
  111. package/dist/http/principal-gate.js +7 -0
  112. package/dist/http/request-guards.d.ts +2 -1
  113. package/dist/http/request-guards.js +14 -14
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +20 -10
  116. package/dist/http/route-ctx.d.ts +15 -7
  117. package/dist/http/routes/a2a-serve.js +11 -7
  118. package/dist/http/routes/approvals-assistant.js +18 -3
  119. package/dist/http/routes/background.d.ts +1 -1
  120. package/dist/http/routes/background.js +1 -1
  121. package/dist/http/routes/capabilities.js +10 -2
  122. package/dist/http/routes/mcp-reconnect.js +1 -1
  123. package/dist/http/routes/runs.d.ts +4 -3
  124. package/dist/http/routes/runs.js +35 -14
  125. package/dist/http/routes/session-sync.js +14 -11
  126. package/dist/http/routes/sessions.js +9 -9
  127. package/dist/http/routes/side-query.js +6 -19
  128. package/dist/http/routes/tasks.js +29 -19
  129. package/dist/http/routes/workflows.js +8 -0
  130. package/dist/http/server.d.ts +4 -0
  131. package/dist/http/server.js +3 -5
  132. package/dist/http/submit-replay.d.ts +15 -8
  133. package/dist/http/submit-replay.js +6 -0
  134. package/dist/http/wire-types.d.ts +7 -9
  135. package/dist/index.d.ts +1 -1
  136. package/dist/index.js +1 -1
  137. package/dist/internal-run-env.d.ts +30 -0
  138. package/dist/internal-run-env.js +11 -0
  139. package/dist/leader/wire.js +3 -2
  140. package/dist/lsp/manager.js +1 -1
  141. package/dist/memory-scope.d.ts +19 -7
  142. package/dist/memory-scope.js +25 -8
  143. package/dist/observability/fail-open.d.ts +31 -3
  144. package/dist/observability/fail-open.js +31 -3
  145. package/dist/observability/metrics.d.ts +0 -1
  146. package/dist/observability/metrics.js +1 -4
  147. package/dist/observability/run-terminal.js +72 -4
  148. package/dist/permission-rule-vocab.d.ts +3 -2
  149. package/dist/permission-rule-vocab.js +1 -1
  150. package/dist/plan-cache-probe.d.ts +3 -2
  151. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  152. package/dist/plugins/caching-session-store.d.ts +42 -22
  153. package/dist/plugins/caching-session-store.js +19 -9
  154. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  155. package/dist/plugins/checkpoint-store-sql.js +57 -11
  156. package/dist/plugins/file-run-store.d.ts +25 -21
  157. package/dist/plugins/file-run-store.js +70 -34
  158. package/dist/plugins/k8s-exec-protocol.js +3 -0
  159. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  160. package/dist/plugins/local-checkpoint-store.js +5 -15
  161. package/dist/plugins/local-session-store.d.ts +66 -7
  162. package/dist/plugins/local-session-store.js +168 -101
  163. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  164. package/dist/plugins/memory-run-store.d.ts +19 -3
  165. package/dist/plugins/memory-run-store.js +38 -3
  166. package/dist/plugins/one-time-migrations.d.ts +165 -39
  167. package/dist/plugins/one-time-migrations.js +51 -144
  168. package/dist/plugins/pg-pool.js +11 -1
  169. package/dist/plugins/pg-session-storage.d.ts +2 -0
  170. package/dist/plugins/pg-session-storage.js +9 -5
  171. package/dist/plugins/remote-env-adb.d.ts +3 -0
  172. package/dist/plugins/remote-env-adb.js +1 -0
  173. package/dist/plugins/remote-env-device.d.ts +3 -0
  174. package/dist/plugins/remote-env-device.js +1 -0
  175. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  176. package/dist/plugins/remote-env-e2b.js +2 -1
  177. package/dist/plugins/remote-env-host.d.ts +23 -1
  178. package/dist/plugins/remote-env-host.js +20 -2
  179. package/dist/plugins/remote-env-k8s.d.ts +264 -41
  180. package/dist/plugins/remote-env-k8s.js +551 -82
  181. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  182. package/dist/plugins/remote-env-local-docker.js +2 -1
  183. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  184. package/dist/plugins/remote-env-ssh.js +1 -0
  185. package/dist/plugins/retention-store-sql.d.ts +4 -1
  186. package/dist/plugins/retention-store-sql.js +11 -9
  187. package/dist/plugins/run-store-sql.d.ts +44 -6
  188. package/dist/plugins/run-store-sql.js +92 -27
  189. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  190. package/dist/plugins/session-store.d.ts +4 -3
  191. package/dist/plugins/session-store.js +1 -1
  192. package/dist/plugins/sql-driver.d.ts +2 -1
  193. package/dist/plugins/store-backend.d.ts +12 -5
  194. package/dist/plugins/store-backend.js +13 -4
  195. package/dist/plugins/store-contracts.d.ts +20 -3
  196. package/dist/plugins/store-open-wait.js +3 -0
  197. package/dist/plugins/tidb-pool.js +15 -3
  198. package/dist/plugins/tidb-session-storage.js +3 -2
  199. package/dist/plugins/tidb-session-store.d.ts +2 -0
  200. package/dist/plugins/tidb-session-store.js +6 -2
  201. package/dist/request-key-closure.d.ts +32 -7
  202. package/dist/request-key-closure.js +25 -9
  203. package/dist/router/route-orchestration.d.ts +3 -11
  204. package/dist/router/route-orchestration.js +0 -4
  205. package/dist/rule-entry-list.js +0 -4
  206. package/dist/rules-consent.js +3 -0
  207. package/dist/run-acceptance.d.ts +51 -0
  208. package/dist/run-acceptance.js +27 -0
  209. package/dist/run-local.d.ts +5 -4
  210. package/dist/run-local.js +23 -17
  211. package/dist/runs.d.ts +7 -11
  212. package/dist/runs.js +6 -16
  213. package/dist/sandbox-egress-policy.d.ts +61 -0
  214. package/dist/sandbox-egress-policy.js +235 -0
  215. package/dist/security.d.ts +7 -0
  216. package/dist/security.js +5 -0
  217. package/dist/server-secret-env.d.ts +10 -4
  218. package/dist/server-secret-env.js +4 -1
  219. package/dist/session-error-wire.d.ts +2 -2
  220. package/dist/session-incarnation.d.ts +46 -2
  221. package/dist/session-incarnation.js +14 -3
  222. package/dist/session-sync-kernel.d.ts +10 -3
  223. package/dist/session-titler.d.ts +5 -3
  224. package/dist/spec-fields.d.ts +24 -8
  225. package/dist/spec-fields.js +21 -2
  226. package/dist/task-workflow.d.ts +6 -4
  227. package/dist/task-workflow.js +3 -2
  228. package/dist/tool-approval.d.ts +78 -6
  229. package/dist/tool-approval.js +7 -3
  230. package/dist/trace/core-keyset-guard.d.ts +6 -5
  231. package/dist/trace/engine-notice-wire.d.ts +26 -1
  232. package/dist/trace/engine-notice-wire.js +7 -2
  233. package/dist/trace/ledger-events.d.ts +9 -3
  234. package/dist/trace/ledger-sink.d.ts +7 -0
  235. package/dist/trace/ledger-sink.js +14 -4
  236. package/dist/trace/project.d.ts +57 -11
  237. package/dist/trace/project.js +54 -8
  238. package/dist/trace/self-minted-wire.d.ts +94 -2
  239. package/dist/trace/self-minted-wire.js +28 -0
  240. package/dist/trace/tool-output-media.d.ts +26 -0
  241. package/dist/trace/tool-output-media.js +57 -0
  242. package/dist/trace/wire-projection-faces.d.ts +7 -1
  243. package/dist/trace/wire-projection-faces.js +2 -1
  244. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  245. package/dist/web-admin-client/apply-effective.js +11 -26
  246. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  247. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  248. package/dist/web-admin-client/candidate-gate.js +9 -2
  249. package/dist/web-admin-client/facade.d.ts +2 -2
  250. package/dist/web-admin-client/http-client.d.ts +3 -1
  251. package/dist/web-admin-client/http-client.js +4 -3
  252. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  253. package/dist/web-admin-client/read-warnings.js +41 -6
  254. package/dist/web-admin-client/types.d.ts +11 -5
  255. package/dist/workspace-cache.d.ts +45 -0
  256. package/dist/workspace-cache.js +30 -0
  257. package/dist/workspace-space-commit.d.ts +116 -0
  258. package/dist/workspace-space-commit.js +117 -0
  259. package/dist/workspace-space.d.ts +75 -9
  260. package/dist/workspace-space.js +52 -1
  261. package/package.json +4 -4
  262. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  263. package/dist/boot/session-incarnation-seat.js +0 -69
  264. package/dist/capabilities/code-review-council.d.ts +0 -49
  265. package/dist/capabilities/code-review-council.js +0 -111
  266. package/dist/capabilities/collab-wire.d.ts +0 -36
  267. package/dist/capabilities/collab-wire.js +0 -2
  268. package/dist/capabilities/collab-workflows.d.ts +0 -47
  269. package/dist/capabilities/collab-workflows.js +0 -90
  270. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  271. package/dist/capabilities/prompts/code-review.js +0 -39
  272. package/dist/capabilities/prompts/team.d.ts +0 -10
  273. package/dist/capabilities/prompts/team.js +0 -6
  274. package/dist/capabilities/team.d.ts +0 -113
  275. package/dist/capabilities/team.js +0 -70
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import vm from "node:vm";
3
3
  import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
4
- import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, } from "@sema-agent/settings-schema/hooks";
4
+ import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
5
5
  import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
6
6
  import { boundedNoticeText } from "../fleet/fleet-bus.js";
7
7
  import { recordFailOpen } from "../observability/fail-open.js";
@@ -59,7 +59,8 @@ function toAssistantTurnError(errorKind) {
59
59
  return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
60
60
  }
61
61
  export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
62
- export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
62
+ export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
63
+ export const MATCHER_ECHO_CHARS = 200;
63
64
  const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
64
65
  function matcherReadsWireToolName(event) {
65
66
  return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
@@ -81,10 +82,7 @@ function engineToolAliases() {
81
82
  return table;
82
83
  }
83
84
  let matcherEvalBox;
84
- function matcherSelectsWithin(matcher, name, budget) {
85
- const remaining = budget.deadlineMs - budget.now();
86
- if (remaining <= 0)
87
- return "over_budget";
85
+ function matcherSelectsWithin(matcher, name, budgetMs) {
88
86
  matcherEvalBox ??= {
89
87
  script: new vm.Script("hookMatcherSelects(matcher, name)"),
90
88
  context: vm.createContext({ hookMatcherSelects, matcher: "", name: "" }),
@@ -92,7 +90,7 @@ function matcherSelectsWithin(matcher, name, budget) {
92
90
  matcherEvalBox.context.matcher = matcher;
93
91
  matcherEvalBox.context.name = name;
94
92
  try {
95
- const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(remaining)) });
93
+ const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(budgetMs)) });
96
94
  return verdict === true ? true : verdict === false ? false : "invalid";
97
95
  }
98
96
  catch (err) {
@@ -101,12 +99,21 @@ function matcherSelectsWithin(matcher, name, budget) {
101
99
  throw err;
102
100
  }
103
101
  }
102
+ function matcherCompileSentence(matcher) {
103
+ try {
104
+ new RegExp(matcher);
105
+ }
106
+ catch (err) {
107
+ return err instanceof Error ? err.message : String(err);
108
+ }
109
+ return "the pattern does not compile as a regular expression";
110
+ }
104
111
  const MATCHER_EVALUATION = {
105
112
  literal: { regex: "inline", literal: "inline" },
106
113
  regex: { regex: "budgeted", literal: "not_applicable" },
107
114
  invalid: { regex: "unreadable", literal: "not_applicable" },
108
115
  };
109
- function deadAliasInMatcher(matcher, depth, budget) {
116
+ function judgeMatcher(matcher, depth, budgetMs) {
110
117
  if (matcher.length > MAX_MATCHER_EVAL_CHARS)
111
118
  return { kind: "unjudged", reason: "oversize" };
112
119
  const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
@@ -114,7 +121,7 @@ function deadAliasInMatcher(matcher, depth, budget) {
114
121
  case "not_applicable":
115
122
  return undefined;
116
123
  case "unreadable":
117
- return { kind: "unjudged", reason: "invalid_regex" };
124
+ return { kind: "invalid_regex" };
118
125
  case "inline":
119
126
  case "budgeted":
120
127
  break;
@@ -123,48 +130,74 @@ function deadAliasInMatcher(matcher, depth, budget) {
123
130
  return unreachable;
124
131
  }
125
132
  }
126
- const selects = (name) => {
127
- const verdict = evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budget);
128
- return verdict === "invalid" ? "invalid_regex" : verdict;
129
- };
133
+ const selects = (name) => (evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs));
134
+ const unanswered = (verdict) => (verdict === "over_budget" ? { kind: "over_budget" } : { kind: "invalid_regex" });
130
135
  for (const [alias, canonical] of engineToolAliases()) {
131
136
  const selectsAlias = selects(alias);
132
137
  if (typeof selectsAlias === "string")
133
- return { kind: "unjudged", reason: selectsAlias };
138
+ return unanswered(selectsAlias);
134
139
  if (!selectsAlias)
135
140
  continue;
136
141
  const selectsCanonical = selects(canonical);
137
142
  if (typeof selectsCanonical === "string")
138
- return { kind: "unjudged", reason: selectsCanonical };
143
+ return unanswered(selectsCanonical);
139
144
  if (!selectsCanonical)
140
145
  return { kind: "dead", alias, canonical };
141
146
  }
142
147
  return undefined;
143
148
  }
144
- export function buildDeadToolMatcherRefusal(issue) {
145
- return {
146
- message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
147
- field: "settings.hooks",
148
- detail: { event: issue.event, alias: issue.alias, canonical: issue.canonical },
149
- };
149
+ export function buildToolMatcherRefusal(issue) {
150
+ switch (issue.reason) {
151
+ case "dead_alias":
152
+ return {
153
+ message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
154
+ field: "settings.hooks",
155
+ detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
156
+ };
157
+ case "over_budget":
158
+ return {
159
+ message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
160
+ field: "settings.hooks",
161
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
162
+ };
163
+ case "invalid_regex":
164
+ return {
165
+ message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
166
+ field: "settings.hooks",
167
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
168
+ };
169
+ default: {
170
+ const unreachable = issue;
171
+ return unreachable;
172
+ }
173
+ }
150
174
  }
151
- export function deadToolMatcherIssue(config, depth, budget = {}) {
152
- const now = budget.now ?? (() => performance.now());
153
- const deadline = { deadlineMs: now() + (budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS), now };
175
+ export function toolMatcherIssue(config, depth, budget = {}) {
176
+ const budgetMs = budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS;
154
177
  for (const [event, groups] of Object.entries(config)) {
155
178
  if (!matcherReadsWireToolName(event))
156
179
  continue;
157
- for (const group of groups ?? []) {
180
+ for (const [groupIndex, group] of (groups ?? []).entries()) {
158
181
  if (group.matcher === undefined)
159
182
  continue;
160
- const verdict = deadAliasInMatcher(group.matcher, depth, deadline);
183
+ const verdict = judgeMatcher(group.matcher, depth, budgetMs);
161
184
  if (verdict === undefined)
162
185
  continue;
163
- if (verdict.kind === "unjudged") {
164
- recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
165
- continue;
186
+ switch (verdict.kind) {
187
+ case "dead":
188
+ return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
189
+ case "over_budget":
190
+ return { reason: "over_budget", event, groupIndex, budgetMs };
191
+ case "invalid_regex":
192
+ return { reason: "invalid_regex", event, groupIndex, matcher: clip(group.matcher, MATCHER_ECHO_CHARS), compileError: clip(matcherCompileSentence(group.matcher), MATCHER_ECHO_CHARS) };
193
+ case "unjudged":
194
+ recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
195
+ break;
196
+ default: {
197
+ const unreachable = verdict;
198
+ return unreachable;
199
+ }
166
200
  }
167
- return { event, alias: verdict.alias, canonical: verdict.canonical };
168
201
  }
169
202
  }
170
203
  return undefined;
@@ -558,19 +591,19 @@ async function runLlmHook(entry, payload, ctx, extra) {
558
591
  clearTimeout(hardTopTimer);
559
592
  }
560
593
  }
561
- async function runHttpHook(entry, payload, ctx) {
594
+ const MAX_BLANKED_REFERENCES_LOGGED = 16;
595
+ async function runHttpHook(entry, payload, ctx, event) {
562
596
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
563
- const allowed = new Set(entry.allowedEnvVars ?? []);
564
- const envSource = hookEnv(ctx);
565
- const headers = { "content-type": "application/json" };
566
- for (const [k, v] of Object.entries(entry.headers ?? {})) {
567
- headers[k] = v.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, name) => {
568
- if (!allowed.has(name))
569
- return whole;
570
- const ev = envSource[name];
571
- return typeof ev === "string" ? ev : whole;
597
+ const interpolated = interpolateHookEnvVars(entry.headers ?? {}, entry.allowedEnvVars ?? [], hookEnv(ctx));
598
+ if (interpolated.blanked.length > 0) {
599
+ ctx.logger.warn("hook_http_header_env_blanked", {
600
+ event,
601
+ url: buildDisplaySafeUrl(entry.url),
602
+ blankedCount: interpolated.blanked.length,
603
+ blanked: interpolated.blanked.slice(0, MAX_BLANKED_REFERENCES_LOGGED).map(({ header, name, reason }) => ({ header, name, reason })),
572
604
  });
573
605
  }
606
+ const headers = { "content-type": "application/json", ...interpolated.headers };
574
607
  const ac = new AbortController();
575
608
  const timer = setTimeout(() => ac.abort(), timeoutMs);
576
609
  try {
@@ -732,7 +765,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
732
765
  onceFired.add(onceKey);
733
766
  if (entry.type === "prompt" || entry.type === "agent")
734
767
  llmEntries++;
735
- const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx)
768
+ const run = entry.type === "http" ? await runHttpHook(entry, payload, ctx, event)
736
769
  : entry.type === "prompt" || entry.type === "agent" ? await runLlmHook(entry, payload, ctx, entry.type === "prompt" ? llmExtra : { system: ccAgentHookSystemFor(event), event })
737
770
  : await runCommandHook(entry, payload, ctx);
738
771
  if (run.spawnError) {
@@ -1,19 +1,20 @@
1
1
  import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isPermissionMode, isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
- import { HttpError } from "../security.js";
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
6
  import { autonomyModeFloor } from "../runtime-governance.js";
7
7
  import { taskBodyKeyIssue } from "../request-key-closure.js";
8
8
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
9
9
  import * as facts from "../capability-facts.js";
10
- import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
10
+ import { buildToolMatcherRefusal, parseHooksConfig, toolMatcherIssue } from "../hooks/hook-runner.js";
11
11
  import { judgeRequestWebSearch } from "../plugins/web-search.js";
12
12
  import { normalizeApproachNotice, skillToolListOk, skillToolListRefusal, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
13
13
  import { MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
14
14
  import { verifyRoundsFromBody } from "./verify-rounds.js";
15
15
  import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
16
16
  import { resolveDevicePlacementIntent } from "./device-placement.js";
17
+ import { createRunAcceptance } from "../run-acceptance.js";
17
18
  import { sendError, httpErrorCode } from "./send.js";
18
19
  import { ABSOLUTE_HOST_PATH_MAX, isAbsoluteHostPath, WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
19
20
  const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
@@ -62,6 +63,7 @@ function refuseUnavailableModel(res, config, field, ref) {
62
63
  sendError(res, 400, "request.model_unavailable", message);
63
64
  return true;
64
65
  }
66
+ const modelIdOf = (model) => (typeof model === "string" ? model : model?.id);
65
67
  export function createAdmission(ctx) {
66
68
  const { deps, readJson, runnerFor } = ctx;
67
69
  async function prepareSpec(req, res, bodyIn, onTypedFailure, legFacts) {
@@ -85,9 +87,12 @@ export function createAdmission(ctx) {
85
87
  sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
86
88
  return null;
87
89
  }
88
- if (typeof body.sessionId === "string" && body.sessionId.length > 64) {
89
- sendError(res, 400, "request.field_invalid", "sessionId must be at most 64 characters");
90
- return null;
90
+ {
91
+ const sessionIdIssue = sessionIdShapeIssue(body.sessionId);
92
+ if (sessionIdIssue) {
93
+ sendError(res, 400, "request.id_invalid", sessionIdIssue);
94
+ return null;
95
+ }
91
96
  }
92
97
  if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
93
98
  sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
@@ -212,9 +217,9 @@ export function createAdmission(ctx) {
212
217
  sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
213
218
  return null;
214
219
  }
215
- const deadMatcher = deadToolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
216
- if (deadMatcher !== undefined) {
217
- const refusal = buildDeadToolMatcherRefusal(deadMatcher);
220
+ const toolMatcher = toolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
221
+ if (toolMatcher !== undefined) {
222
+ const refusal = buildToolMatcherRefusal(toolMatcher);
218
223
  sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
219
224
  return null;
220
225
  }
@@ -502,7 +507,7 @@ export function createAdmission(ctx) {
502
507
  return null;
503
508
  }
504
509
  }
505
- for (const key of ["excludeTools", "deferTools"]) {
510
+ for (const key of ["allowTools", "excludeTools", "deferTools"]) {
506
511
  const v = body[key];
507
512
  if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
508
513
  sendError(res, 400, "request.field_invalid", `${key} must be an array of non-empty tool-name strings`);
@@ -637,7 +642,9 @@ export function createAdmission(ctx) {
637
642
  store: deps.deviceStore,
638
643
  enrollment: deps.deviceEnrollment,
639
644
  });
640
- return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
645
+ const model = modelIdOf(spec.model);
646
+ 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 } : {}) };
641
648
  }
642
649
  catch (err) {
643
650
  if (err instanceof HttpError) {
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { type RunInternals } from "@sema-agent/core";
17
17
  import type { RunStore } from "../plugins/store-backend.js";
18
- import type { RunRecord } from "../plugins/store-contracts.js";
18
+ import type { RunIdemBinding, RunRecord } from "../plugins/store-contracts.js";
19
19
  import type { PreparedTaskSubmission, RouteCtxBase } from "./route-ctx.js";
20
20
  /**
21
21
  * 🔴 **TRUSTED run internals** —— 由**部署方**(不是模型、不是请求体、不是工具参数)铸给这一跑的
@@ -35,8 +35,9 @@ export interface DurableRunLegArgs {
35
35
  prepared: PreparedTaskSubmission;
36
36
  /** 调用方自带的 taskId(`POST /v1/runs` 的 `body.taskId` 幂等键);缺席 ⇒ 本腿铸 uuidv7。 */
37
37
  clientTaskId?: string;
38
- /** 这次提交的幂等键(`idemKeyOf(raw, caller, "run")`)—— 落 `task_run.idem_key`;缺席 = 调用方没带 `Idempotency-Key`。 */
39
- idemKey?: string;
38
+ /** 这次提交的账本绑定(键 `idemKeyOf(raw, caller, "run")` + 请求体指纹)—— 落 `task_run.idem_key` / `idem_body_fingerprint`;
39
+ * 缺席 = 调用方没带 `Idempotency-Key`。 */
40
+ idem?: RunIdemBinding;
40
41
  /** 🔒 这条 run 的 **fleet 行 scope** —— 必须与 `GET /v1/fleet/stream` 过滤用的是同一个身份,且绝不是
41
42
  * 自报的 `x-agent-principal` 头(自报头会把行打到受害者的 scope 上,或打到 `default` 而真属主的流把它
42
43
  * 过滤掉)。提交门传 `gatedPrincipal(req) ?? "default"`;`/decide` 的第三条车道传 resume 家族的
@@ -71,7 +72,7 @@ export type DurableRunLegOutcome = {
71
72
  /**
72
73
  * 建行 + 驱动。返回 `202` = 新行已建并在跑(体带 `taskId`/`sessionId`);`409` = 会话被别的 run 占着,
73
74
  * **没有**建行(体 = `buildActiveRunConflict` 的既有形,含 `errorCode: "conflict.session_active_run"`);
74
- * `existing` = 这次提交的钥匙(`idemKey` / `clientTaskId`)已经指向一条行,没建新行。
75
+ * `existing` = 这次提交的钥匙(`idem.key` / `clientTaskId`)已经指向一条行,没建新行。
75
76
  * 协议渲染归调用方(HTTP `sendJson` / A2A 协议对象 / `/decide` 的受理体)。
76
77
  */
77
78
  export declare function mintAndDriveDurableRun(ctx: RouteCtxBase, runStore: RunStore, args: DurableRunLegArgs): Promise<DurableRunLegOutcome>;
@@ -15,13 +15,11 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
15
15
  const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
16
16
  const sessionId = args.prepared.spec.sessionId ?? uuidv7();
17
17
  const taskId = args.clientTaskId ?? uuidv7();
18
- const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", { ...runMeta(args.prepared, args.source), idemKey: args.idemKey ?? null });
18
+ const created = await runStore.createRun(taskId, sessionId, args.prepared.auth?.principal ?? null, deps.instanceId ?? "default", { ...runMeta(args.prepared, args.source), idem: args.idem ?? null });
19
19
  if (created.ok)
20
20
  clearTurnActivity(taskId);
21
21
  if (created.ok)
22
22
  recordResourceWindow(taskId, args.prepared.spec.resourceSuspend);
23
- if (created.ok)
24
- deps.sessionTitler?.maybeTitle(sessionId, args.prepared.spec.objective, typeof args.prepared.spec.model === "string" ? args.prepared.spec.model : args.prepared.spec.model?.id);
25
23
  if (created.existing)
26
24
  return { existing: created.existing };
27
25
  if (!created.ok) {
@@ -65,7 +63,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
65
63
  windowMs: deps.config.streamApproval?.windowMs,
66
64
  windowMarginMs: deps.config.streamAskWindowMarginMs,
67
65
  }
68
- : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger);
66
+ : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger, args.prepared.acceptance);
69
67
  return { status: 202, body: { taskId, sessionId, status: "running" } };
70
68
  }
71
69
  //# sourceMappingURL=durable-run-leg.js.map
@@ -2,6 +2,7 @@ import type { IncomingMessage } from "node:http";
2
2
  import type { ServiceConfig } from "../config-types.js";
3
3
  import type { WiringManifestReceipt } from "./wire-types.js";
4
4
  import type { SubmitReplayFace } from "./submit-replay.js";
5
+ import type { RunIdemBinding } from "../plugins/store-contracts.js";
5
6
  /**
6
7
  * 同进程的**在飞合流** —— 并发同键的提交共享同一个 promise(避免同一进程里双起)。它**不是**重放的真源。
7
8
  *
@@ -78,14 +79,35 @@ export declare function idemIdentityOf(caller: IdemCaller): string;
78
79
  */
79
80
  export declare function idemKeyOf(raw: string, caller: IdemCaller, family: IdemFamily): string;
80
81
  /**
81
- * 在飞合流的键 = 提交幂等键 × 这条腿的**回执形**(`durable` / `sync`)× 这次请求自带的 taskId。
82
+ * 一次提交的**请求体指纹** —— HTTP 三条提交腿(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)同一只规范化函数:
83
+ * `sha256(canonicalJson(体 − taskId))` 的 hex(定宽 64)。
82
84
  *
83
- * 合流交出的是**首个请求那条腿的结局**,所以只有「同一条 run、同一种回执形、同一把 taskId」的请求才许合流:durable 腿
84
- * 合到同步腿的在飞结局上会拿到 200 引擎结果而不是 202 收据;带另一个 taskId 的请求合进来就绕过了「key 指向别的行」
85
- * 那条 409。形不同(或 taskId 不同)的请求各自走到建行,由店的原子赢或观察 + 唯一回执函数作答 —— 仍是一条 run,
86
- * 各拿自己腿的回执形。同步腿与流式腿同形(流式腿把同一份体放进 `done.result`),合流照旧。
85
+ * 幂等 = 同一调用方、同一键、**同一请求**:账本行记下绑定 `Idempotency-Key` 的那次请求的指纹(`task_run.idem_body_fingerprint`),
86
+ * 同键再投而指纹不同 ⇒ 唯一回执函数答 409(`http/submit-replay.ts`)。规范化只有两步:
87
+ * · JSON 按键递归排序(唯一序列化器 `canonicalJson`;值为 `undefined` 的键与缺席同义)—— 键序不同的同一份体是同一请求;
88
+ * · 去掉 `taskId` —— 它是同一条律的**另一把钥匙**(调用方自铸的主键),由回执函数的「钥匙指向别的行 ⇒ `conflict.run_exists`」判,
89
+ * 不是请求内容(换腿重投:流式腿不带 → durable 腿带回 `x-task-id`,仍是同一请求)。
90
+ * 头一概不进:身份已在键里(`idemKeyOf` 的身份位),其余头(`Idempotency-Key` 本身、`x-detach-on-disconnect` 一类连接语义)不是提交内容。
91
+ * 入参是**线上收到的那份体**(`readJson` 的产物),不是 `prepareSpec` 之后服务端盖过章的体 —— 指纹只由调用方看得见的字节构成,
92
+ * 否则两台配置不同的副本对同一次重试算出两只指纹。
87
93
  */
88
- export declare function inflightKeyOf(idemKey: string, face: SubmitReplayFace, clientTaskId?: string): string;
94
+ export declare function submissionFingerprintOf(body: unknown): string;
95
+ /**
96
+ * HTTP 提交腿(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)的**账本绑定**唯一铸法:键 = `idemKeyOf(raw, caller, "run")`,
97
+ * 指纹 = {@link submissionFingerprintOf}(线上收到的那份体)。三条腿在读到 `Idempotency-Key` 的同一处调它 —— 键与指纹同铸,不各拼一份。
98
+ * (A2A 的绑定不经这里:它的原始键就是消息内容指纹,绑定 = `{ key: idemKeyOf(指纹, …), bodyFingerprint: 指纹 }`,见 `routes/a2a-serve.ts`。)
99
+ */
100
+ export declare function submissionIdemOf(raw: string, caller: IdemCaller, body: unknown): RunIdemBinding;
101
+ /**
102
+ * 在飞合流的键 = 这次提交的**账本绑定**(幂等键 × 请求体指纹)× 这条腿的**回执形**(`durable` / `sync`)× 这次请求自带的 taskId。
103
+ *
104
+ * 合流交出的是**首个请求那条腿的结局**,所以只有「同一请求、同一条 run、同一种回执形、同一把 taskId」的请求才许合流:同键异体的
105
+ * 请求合进来就是静默拿到别的请求的结局(正是 409 `conflict.idempotency_payload_mismatch` 要拦的那件事);durable 腿合到同步腿的在飞
106
+ * 结局上会拿到 200 引擎结果而不是 202 收据;带另一个 taskId 的请求合进来就绕过了「key 指向别的行」那条 409。不许合流的请求各自走到
107
+ * 读行 / 建行,由店的原子赢或观察 + 唯一回执函数作答 —— 同体的仍是一条 run、各拿自己腿的回执形,异体的拿 409。同步腿与流式腿同形
108
+ * (流式腿把同一份体放进 `done.result`),合流照旧。
109
+ */
110
+ export declare function inflightKeyOf(idem: RunIdemBinding, face: SubmitReplayFace, clientTaskId?: string): string;
89
111
  /**
90
112
  * 提交腿的**运行结局**(在飞合流共享的那一只)—— `status` + `body` 是它一直以来的两位;S-528 起多一位
91
113
  * `wiringManifest`。
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { canonicalJson } from "../canonical-json.js";
2
3
  import { gatedPrincipal } from "./principal-gate.js";
3
4
  export class IdempotencyCache {
4
5
  ttlMs;
@@ -77,7 +78,14 @@ export function idemIdentityOf(caller) {
77
78
  export function idemKeyOf(raw, caller, family) {
78
79
  return createHash("sha256").update(`${idemIdentityOf(caller)}\x1f${idemFamilyTag(family)}\x1f${raw}`).digest("hex");
79
80
  }
80
- export function inflightKeyOf(idemKey, face, clientTaskId) {
81
- return `${idemKey}\x1f${face}\x1f${clientTaskId ?? ""}`;
81
+ export function submissionFingerprintOf(body) {
82
+ const content = body !== null && typeof body === "object" && !Array.isArray(body) ? { ...body, taskId: undefined } : body;
83
+ return createHash("sha256").update(canonicalJson(content)).digest("hex");
84
+ }
85
+ export function submissionIdemOf(raw, caller, body) {
86
+ return { key: idemKeyOf(raw, caller, "run"), bodyFingerprint: submissionFingerprintOf(body) };
87
+ }
88
+ export function inflightKeyOf(idem, face, clientTaskId) {
89
+ return `${idem.key}\x1f${idem.bodyFingerprint}\x1f${face}\x1f${clientTaskId ?? ""}`;
82
90
  }
83
91
  //# sourceMappingURL=idempotency.js.map
@@ -92,6 +92,20 @@ export declare function sendUnauthorized(res: ServerResponse, face: Unauthorized
92
92
  * 那样「谁多给了 operator 一条路」在代码上看得见,而不是被偷偷塞进共享谓词里。
93
93
  */
94
94
  export declare function runOwnerReadable(principal: string | undefined, owner: string | null, config: ServiceConfig): boolean;
95
+ /**
96
+ * 🔴 **「这台部署有没有身份边界」的唯一判据**(S-1452)。dev-open = 没配 service token(单一或 per-system)、没配只读 trace token、
97
+ * 也没开 `REQUIRE_PRINCIPAL`:请求里没有任何东西替主体头作保,头是谁都能自报的**标签**(过滤 / 展示用),不是身份。
98
+ * 三处读它、不各写一份:会话列表的 fleet-wide 第一臂(`isFleetWide`)、会话写面的 fleet-wide 第一臂(`sessionOwnerScopeForWrite`)、
99
+ * 会话面的属主谓词({@link sessionOwnerReadable})—— 于是「调用方是谁」在列表与单会话面上只有一只读法。
100
+ */
101
+ export declare function devOpenDeployment(config: ServiceConfig): boolean;
102
+ /**
103
+ * **「这个调用方能不能动这只会话」的唯一谓词**(会话面:视图 / head / 事件流 / 记忆状态与自助抹除 / 改名置顶 / 后台任务 / MCP 重连 /
104
+ * 面板的上一腿 MCP 投影)。= {@link devOpenDeployment} ∨ {@link runOwnerReadable}:没有身份边界的部署上不钉任何人(与会话列表同读,
105
+ * 列表把这只会话列给谁,谁就读得到它);有身份边界的部署上就是 run 面那一条谓词,一字不变。
106
+ * run 面(poll / events / cancel …)不读本谓词 —— 那是 run 资源的门,另一只。
107
+ */
108
+ export declare function sessionOwnerReadable(principal: string | undefined, owner: string | null, config: ServiceConfig): boolean;
95
109
  /** The EXPLICIT operator gate (IMAGE-API-DESIGN.md §P2.4a MUST-FIX) — NOT the bare `isOperator` (whose empty-list
96
110
  * "true-for-all" is an unrestricted-write exec door the instant OPERATOR_PRINCIPALS is empty). Requires a non-empty
97
111
  * operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
@@ -93,6 +93,13 @@ export function runOwnerReadable(principal, owner, config) {
93
93
  return true;
94
94
  return !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
95
95
  }
96
+ export function devOpenDeployment(config) {
97
+ const anyServiceToken = Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
98
+ return !anyServiceToken && !config.traceToken && !config.requirePrincipal;
99
+ }
100
+ export function sessionOwnerReadable(principal, owner, config) {
101
+ return devOpenDeployment(config) || runOwnerReadable(principal, owner, config);
102
+ }
96
103
  export function explicitOperatorOk(principal, operatorPrincipals) {
97
104
  return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
98
105
  }
@@ -84,7 +84,8 @@ export declare function createRequestGuards(ctx: {
84
84
  fleetWide: boolean;
85
85
  gateOwner: string | null;
86
86
  };
87
- runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session") => boolean;
87
+ runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
88
+ sessionOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null) => boolean;
88
89
  runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
89
90
  sessionId: string | null;
90
91
  }, face: string, notFoundError?: string) => boolean;
@@ -2,7 +2,7 @@ import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
2
2
  import { HttpError, principalFrom } from "../security.js";
3
3
  import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
4
4
  import { sendError } from "./send.js";
5
- import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
5
+ import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sessionOwnerReadable, devOpenDeployment, sendPrincipalRefusal } from "./principal-gate.js";
6
6
  import { quotaWindowResetsAt } from "../observability/cost-quota.js";
7
7
  const MAX_BODY = 8 * 1024 * 1024;
8
8
  export function leaseRefusalOf(adm) {
@@ -150,9 +150,8 @@ export function createRequestGuards(ctx) {
150
150
  }
151
151
  }
152
152
  function isFleetWide(req) {
153
- const { authToken: at, traceToken: tt } = deps.config;
154
- const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
155
- return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
153
+ const tt = deps.config.traceToken;
154
+ return devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined || (!!tt && authorized(req, tt));
156
155
  }
157
156
  function sessionOwnerScope(req) {
158
157
  const fleetWide = isFleetWide(req);
@@ -160,27 +159,28 @@ export function createRequestGuards(ctx) {
160
159
  return { fleetWide, gateOwner };
161
160
  }
162
161
  function sessionOwnerScopeForWrite(req) {
163
- const { authToken: at, traceToken: tt } = deps.config;
164
- const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
165
- const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
162
+ const fleetWide = devOpenDeployment(deps.config) || systemFor(req, deps.config) !== undefined;
166
163
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
167
164
  return { fleetWide, gateOwner };
168
165
  }
169
- function runOwnerOk(req, res, owner, notFoundForm = "run") {
166
+ function ownerGate(req, res, readable, notFound) {
170
167
  const principal = gatedPrincipal(req, deps.config);
171
168
  if (deps.config.requirePrincipal && !principal) {
172
169
  sendPrincipalRefusal(res, req, deps.config);
173
170
  return false;
174
171
  }
175
- if (!runOwnerReadable(principal, owner, deps.config)) {
176
- if (notFoundForm === "session")
177
- sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
178
- else
179
- sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
172
+ if (!readable(principal)) {
173
+ notFound();
180
174
  return false;
181
175
  }
182
176
  return true;
183
177
  }
178
+ function runOwnerOk(req, res, owner) {
179
+ return ownerGate(req, res, (principal) => runOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE));
180
+ }
181
+ function sessionOwnerOk(req, res, owner) {
182
+ return ownerGate(req, res, (principal) => sessionOwnerReadable(principal, owner, deps.config), () => sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE));
183
+ }
184
184
  const sessionAssertAbsentWarned = new Set();
185
185
  function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
186
186
  if (!run.sessionId)
@@ -229,6 +229,6 @@ export function createRequestGuards(ctx) {
229
229
  }
230
230
  return Buffer.concat(chunks);
231
231
  }
232
- return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
232
+ return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, sessionOwnerOk, runSessionAcceptOk, readJson, readRawBody };
233
233
  }
234
234
  //# sourceMappingURL=request-guards.js.map
@@ -15,7 +15,7 @@ import type { IncomingMessage } from "node:http";
15
15
  import { CheckpointError, type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
16
16
  import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
- import { type HostDecision } from "../host-decision.js";
18
+ import type { HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
20
  import type { RouteCtxBase, DriveResumeArgs, HostLegOutcome, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
@@ -74,7 +74,7 @@ export declare function createResumeLegs(ctx: {
74
74
  readonly ok: true;
75
75
  readonly prepared: PreparedPlanReview;
76
76
  }>;
77
- drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
77
+ drivePlanReview: (prepared: PreparedPlanReview, decision: "approve" | "edit" | "reject", editedPlan: string | undefined, reason: string | undefined, hostDecision: HostDecision, req: IncomingMessage | undefined, acceptEarly?: boolean, permissionModeAfter?: PlanApprovePermissionModeAfter) => Promise<{
78
78
  status: number;
79
79
  body: object;
80
80
  }>;