@sema-agent/server 7.89.0 → 7.91.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adoption/plan.js +11 -0
- package/dist/boot/fatal-exit.d.ts +3 -0
- package/dist/boot/fatal-exit.js +5 -0
- package/dist/boot/resolve-spec.js +2 -0
- package/dist/boot/runner-deps.d.ts +6 -0
- package/dist/boot/runner-deps.js +11 -0
- package/dist/boot/session-faces.d.ts +6 -0
- package/dist/boot/session-faces.js +10 -2
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-06-runners.js +2 -2
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-09-leader.js +2 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +1 -0
- package/dist/boot/stores.d.ts +2 -0
- package/dist/boot/stores.js +11 -3
- package/dist/brain.js +32 -19
- package/dist/capabilities/memory-notice.d.ts +7 -1
- package/dist/capabilities/memory-notice.js +6 -6
- package/dist/capabilities/team.d.ts +37 -0
- package/dist/config-catalog.js +6 -18
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -5
- package/dist/cross-session-settings.d.ts +6 -2
- package/dist/hooks/hook-llm.js +10 -6
- package/dist/hooks/hook-runner.js +19 -2
- package/dist/http/admission.js +9 -5
- package/dist/http/routes/capabilities.js +6 -3
- package/dist/http/routes/rules.d.ts +11 -0
- package/dist/http/routes/rules.js +54 -5
- package/dist/http/routes/session-sync.js +8 -2
- package/dist/http/routes/sessions-list.js +6 -1
- package/dist/http/routes/sessions.js +16 -14
- package/dist/http/server.d.ts +12 -0
- package/dist/http/wire-types.d.ts +12 -0
- package/dist/main.js +2 -1
- package/dist/model-route-url.d.ts +8 -0
- package/dist/model-route-url.js +17 -8
- package/dist/peer-directory.d.ts +114 -0
- package/dist/peer-directory.js +136 -0
- package/dist/permission-rule-vocab.d.ts +27 -1
- package/dist/permission-rule-vocab.js +8 -0
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +86 -0
- package/dist/plugins/mailbox-recipient-lifecycle.js +10 -0
- package/dist/plugins/mailbox-store-sql.d.ts +18 -2
- package/dist/plugins/mailbox-store-sql.js +77 -1
- package/dist/plugins/memory-embedder.d.ts +0 -6
- package/dist/plugins/memory-embedder.js +10 -16
- package/dist/plugins/store-contracts.d.ts +36 -1
- package/dist/plugins/store-contracts.js +10 -0
- package/dist/request-key-closure.js +2 -1
- package/dist/rules-consent.d.ts +74 -1
- package/dist/rules-consent.js +81 -1
- package/dist/runtime-governance.d.ts +16 -7
- package/dist/runtime-governance.js +27 -7
- package/dist/session-policy-wire.d.ts +103 -0
- package/dist/session-policy-wire.js +66 -0
- package/dist/task-settings.d.ts +12 -2
- package/dist/task-settings.js +7 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +7 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/redact.d.ts +96 -11
- package/dist/trace/redact.js +155 -11
- package/dist/trace/sema-provenance.d.ts +7 -6
- package/package.json +3 -3
package/dist/config-catalog.js
CHANGED
|
@@ -5,6 +5,7 @@ import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-sear
|
|
|
5
5
|
import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
|
|
6
6
|
import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
|
|
7
7
|
import { MODEL_PROVIDER_WORDS } from "./model-provider.js";
|
|
8
|
+
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
8
9
|
export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
|
|
9
10
|
export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
|
|
10
11
|
export const CATALOG_DANGER_AXES = ["security", "billing", "data"];
|
|
@@ -55,7 +56,7 @@ export const CONFIG_CATALOG = [
|
|
|
55
56
|
r("ALLOW_UNAUTHED_WRITES", "auth", "boolean", "无 service token 时仍接受任务提交(本地开发逃生口;缺省 fail-closed 拒提交)", { staticDefault: "false", danger: SEC, configKey: "allowUnauthedWrites" }),
|
|
56
57
|
r("ANTHROPIC_API_KEY", "modelPlane", "secret", "Anthropic 协议路线的 x-api-key 凭据(与 AUTH_TOKEN 二选一,AUTH_TOKEN 优先;与 BASE_URL 同在时可推断 provider)", { danger: [...SEC, ...BILL] }),
|
|
57
58
|
r("ANTHROPIC_AUTH_TOKEN", "modelPlane", "secret", "Anthropic 兼容生态的 Bearer 凭据(设了则不发 x-api-key)", { danger: [...SEC, ...BILL] }),
|
|
58
|
-
r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo`;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`)"),
|
|
59
|
+
r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo`;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment` —— fragment 从不上线,后缀会落进被丢弃的那一半)"),
|
|
59
60
|
r("ANTHROPIC_CACHE_BREAKPOINTS", "modelPlane", "boolean", "Anthropic 路线 prompt-cache breakpoints 开关", { staticDefault: "true" }),
|
|
60
61
|
r("ANTHROPIC_MAX_RETRIES", "modelPlane", "number", "Anthropic 路线 SDK 每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,core/SDK 默认当家" }),
|
|
61
62
|
r("ANTHROPIC_VERSION", "modelPlane", "string", "Anthropic API version 头(缺省由 SDK 定)"),
|
|
@@ -282,8 +283,8 @@ export const CONFIG_CATALOG = [
|
|
|
282
283
|
r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
|
|
283
284
|
r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
|
|
284
285
|
r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
|
|
285
|
-
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
|
|
286
|
-
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`)", { configKey: "gatewayFallbackUrls" }),
|
|
286
|
+
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
|
|
287
|
+
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
|
|
287
288
|
r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
|
|
288
289
|
r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
|
|
289
290
|
r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
|
|
@@ -478,19 +479,6 @@ function primitiveConfigValue(config, key) {
|
|
|
478
479
|
return v;
|
|
479
480
|
return undefined;
|
|
480
481
|
}
|
|
481
|
-
const REDACTED_UNPARSEABLE_URL = "(redacted:unparseable-url)";
|
|
482
|
-
function stripUrlUserinfo(v) {
|
|
483
|
-
if (!URL.canParse(v))
|
|
484
|
-
return REDACTED_UNPARSEABLE_URL;
|
|
485
|
-
const u = new URL(v);
|
|
486
|
-
u.username = "";
|
|
487
|
-
u.password = "";
|
|
488
|
-
if (u.search === "" && u.hash === "" && u.href === v)
|
|
489
|
-
return v;
|
|
490
|
-
u.search = "";
|
|
491
|
-
u.hash = "";
|
|
492
|
-
return u.href;
|
|
493
|
-
}
|
|
494
482
|
function sanitizeEnumValue(spec, v) {
|
|
495
483
|
const words = spec.echoWords ?? spec.enumValues;
|
|
496
484
|
if (v === null)
|
|
@@ -511,9 +499,9 @@ function sanitizeValue(spec, v) {
|
|
|
511
499
|
const type = spec.type;
|
|
512
500
|
if (type === "url") {
|
|
513
501
|
if (typeof v === "string")
|
|
514
|
-
return { value: v.includes(",") ? v.split(",").map((s) =>
|
|
502
|
+
return { value: v.includes(",") ? v.split(",").map((s) => buildDisplaySafeUrl(s.trim())).filter((s) => s.length > 0) : buildDisplaySafeUrl(v) };
|
|
515
503
|
if (Array.isArray(v))
|
|
516
|
-
return { value: v.map((s) =>
|
|
504
|
+
return { value: v.map((s) => buildDisplaySafeUrl(s)) };
|
|
517
505
|
return { value: v };
|
|
518
506
|
}
|
|
519
507
|
if (type === "csv" && typeof v === "string")
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
|
|
2
|
-
import type
|
|
2
|
+
import { type Autonomy } from "./runtime-governance.js";
|
|
3
3
|
import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
|
|
4
4
|
/** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
|
|
5
5
|
* every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
|
package/dist/config.js
CHANGED
|
@@ -17,9 +17,10 @@ import { readFaceForPosture } from "./read-face-posture.js";
|
|
|
17
17
|
import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
|
|
18
18
|
import {} from "./posture-source.js";
|
|
19
19
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
20
|
+
import { AUTONOMY_MODES } from "./runtime-governance.js";
|
|
20
21
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
21
22
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
22
|
-
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
23
|
+
import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
|
|
23
24
|
export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
|
|
24
25
|
function csv(name) {
|
|
25
26
|
return (process.env[name] ?? "")
|
|
@@ -27,7 +28,6 @@ function csv(name) {
|
|
|
27
28
|
.map((s) => s.trim())
|
|
28
29
|
.filter(Boolean);
|
|
29
30
|
}
|
|
30
|
-
const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
31
31
|
export function parseAutonomy(raw) {
|
|
32
32
|
const v = raw?.trim();
|
|
33
33
|
if (!v)
|
|
@@ -1972,9 +1972,9 @@ function parseA2aServe() {
|
|
|
1972
1972
|
throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
|
|
1973
1973
|
`Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
|
|
1974
1974
|
}
|
|
1975
|
-
const
|
|
1976
|
-
if (
|
|
1977
|
-
throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${
|
|
1975
|
+
const a2aCredentialScan = redactSecrets(parsed.href);
|
|
1976
|
+
if (a2aCredentialScan !== parsed.href) {
|
|
1977
|
+
throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aCredentialScan}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
|
|
1978
1978
|
`so it must not carry credentials of ANY shape: no userinfo, no credential-named query parameter (api_key/token/…), no token-shaped path segment or parameter value. ` +
|
|
1979
1979
|
`Keep A2A_SERVE_URL to scheme://host[:port][/path] and move the secret into the caller's headers.`);
|
|
1980
1980
|
}
|
|
@@ -28,8 +28,12 @@
|
|
|
28
28
|
* ### 另一件如实:整条车道今日未挂
|
|
29
29
|
*
|
|
30
30
|
* 两个座位在引擎侧都**以 `RunnerDeps.peerDirectory` 在场为前提**(座缺席 ⇒ 跨会话车道整条不挂,
|
|
31
|
-
* 每个面与 pre-385 构建逐字节相同)
|
|
32
|
-
*
|
|
31
|
+
* 每个面与 pre-385 构建逐字节相同)。
|
|
32
|
+
*
|
|
33
|
+
* ✅ **7.91.0:目录席已接线**([ref],`boot/runner-deps.ts` 的三合取:会话枚举面 ∧ mailbox 店 ∧ 收件人生命周期面)
|
|
34
|
+
* —— 旧文字写的是「另一件欠账 …… 就位待命而非生效中」,如实订正:本文件铸的 managed 层在**有席的部署**上
|
|
35
|
+
* 真的在治理 drain 点的判决了;缺任一合取项的部署(典型 = 无耐久 mailbox 的单机形)上车道整条不挂,层仍是
|
|
36
|
+
* 就位待命。当初那句「目录席到货那天零改动即生效」已兑现:本文件一个字节未改。
|
|
33
37
|
*/
|
|
34
38
|
import type { CrossSessionDialogExpiry, CrossSessionInboundSetting, CrossSessionInboundSettingLayers } from "@sema-agent/core";
|
|
35
39
|
/** 引擎层形的层名 —— 从 core 的类型上取,**引擎加一层 = 本文件编译错**(下面两处闭集都锁在这个联合上)。 */
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { adjudicateModelRoute,
|
|
1
|
+
import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
|
|
2
2
|
import { matchCatalogModel } from "../model-select.js";
|
|
3
|
-
import { redactSecrets } from "../trace/redact.js";
|
|
3
|
+
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
4
4
|
export function buildHookFailureText(raw) {
|
|
5
5
|
if (raw === "")
|
|
6
6
|
return raw;
|
|
@@ -53,9 +53,13 @@ export function createHookLlm(deps) {
|
|
|
53
53
|
});
|
|
54
54
|
const verdict = adjudicateModelRoute({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
|
|
55
55
|
if (!verdict.ok) {
|
|
56
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `hook model "${pick.catalogRef}" cannot be routed (${verdict.code}) — ` +
|
|
59
|
+
buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
|
|
60
|
+
};
|
|
57
61
|
}
|
|
58
|
-
if ((verdict.posture === "unpinned" || verdict.posture === "keyless") &&
|
|
62
|
+
if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
|
|
59
63
|
return {
|
|
60
64
|
ok: false,
|
|
61
65
|
error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
|
|
@@ -91,7 +95,7 @@ export function createHookLlm(deps) {
|
|
|
91
95
|
const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
|
|
92
96
|
if (!routed.ok)
|
|
93
97
|
return routed;
|
|
94
|
-
url =
|
|
98
|
+
url = joinRequestUrl(pick.baseUrl, "/v1/messages");
|
|
95
99
|
headers = {
|
|
96
100
|
"content-type": "application/json",
|
|
97
101
|
"anthropic-version": config.anthropic?.version ?? "2023-06-01",
|
|
@@ -112,7 +116,7 @@ export function createHookLlm(deps) {
|
|
|
112
116
|
if (!routed.ok)
|
|
113
117
|
return routed;
|
|
114
118
|
const hookKey = routed.credential.apiKey;
|
|
115
|
-
url =
|
|
119
|
+
url = joinRequestUrl(pick.baseUrl, "/chat/completions");
|
|
116
120
|
headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
|
|
117
121
|
body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
|
|
118
122
|
}
|
|
@@ -52,7 +52,9 @@ const BRAIN_TO_TURN_ERROR = {
|
|
|
52
52
|
max_output_tokens: "max_output_tokens",
|
|
53
53
|
};
|
|
54
54
|
function toAssistantTurnError(errorKind) {
|
|
55
|
-
|
|
55
|
+
if (errorKind === undefined)
|
|
56
|
+
return "unknown";
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
|
|
56
58
|
}
|
|
57
59
|
export function parseHooksConfig(raw) {
|
|
58
60
|
const parsed = HooksConfig.safeParse(raw);
|
|
@@ -1148,6 +1150,21 @@ export function createTaskHooks(config, ctx) {
|
|
|
1148
1150
|
}
|
|
1149
1151
|
return hooks;
|
|
1150
1152
|
}
|
|
1153
|
+
const HOOK_SLOT_FOLD = {
|
|
1154
|
+
preToolUse: "custom",
|
|
1155
|
+
preToolUseObservational: "custom",
|
|
1156
|
+
postToolUse: "custom",
|
|
1157
|
+
postToolUseFailure: "custom",
|
|
1158
|
+
postToolBatch: "custom",
|
|
1159
|
+
userPromptSubmit: "custom",
|
|
1160
|
+
stop: "custom",
|
|
1161
|
+
preCompact: "custom",
|
|
1162
|
+
timeoutMs: "scalar",
|
|
1163
|
+
postCompact: "observe-void",
|
|
1164
|
+
stopFailure: "observe-void",
|
|
1165
|
+
permissionDenied: "observe-void",
|
|
1166
|
+
};
|
|
1167
|
+
const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
|
|
1151
1168
|
export function composeHooks(deployment, task) {
|
|
1152
1169
|
if (!deployment)
|
|
1153
1170
|
return task;
|
|
@@ -1270,7 +1287,7 @@ export function composeHooks(deployment, task) {
|
|
|
1270
1287
|
};
|
|
1271
1288
|
};
|
|
1272
1289
|
}
|
|
1273
|
-
for (const slot of
|
|
1290
|
+
for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
|
|
1274
1291
|
const ds = d[slot];
|
|
1275
1292
|
const ts = t[slot];
|
|
1276
1293
|
if (ds && ts) {
|
package/dist/http/admission.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
2
2
|
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
3
3
|
import { HttpError } from "../security.js";
|
|
4
|
-
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
4
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
5
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
6
6
|
import { singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
7
7
|
import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
@@ -430,6 +430,10 @@ export function createAdmission(ctx) {
|
|
|
430
430
|
sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
|
|
431
431
|
return null;
|
|
432
432
|
}
|
|
433
|
+
if (body.excludeAllTools !== undefined && typeof body.excludeAllTools !== "boolean") {
|
|
434
|
+
sendError(res, 400, "request.field_invalid", "excludeAllTools must be a boolean");
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
433
437
|
for (const key of ["excludeTools", "deferTools"]) {
|
|
434
438
|
const v = body[key];
|
|
435
439
|
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
@@ -453,8 +457,8 @@ export function createAdmission(ctx) {
|
|
|
453
457
|
}
|
|
454
458
|
{
|
|
455
459
|
const v = body.permissionMode;
|
|
456
|
-
if (v !== undefined && v
|
|
457
|
-
sendError(res, 400, "request.field_invalid",
|
|
460
|
+
if (v !== undefined && !isSettingsPermissionMode(v)) {
|
|
461
|
+
sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
|
|
458
462
|
return null;
|
|
459
463
|
}
|
|
460
464
|
}
|
|
@@ -464,8 +468,8 @@ export function createAdmission(ctx) {
|
|
|
464
468
|
const p = s.permissions;
|
|
465
469
|
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
466
470
|
const m = p.defaultMode;
|
|
467
|
-
if (m !== undefined && m
|
|
468
|
-
sendError(res, 400, "request.field_invalid",
|
|
471
|
+
if (m !== undefined && !isSettingsPermissionMode(m)) {
|
|
472
|
+
sendError(res, 400, "request.field_invalid", `settings.permissions.defaultMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
|
|
469
473
|
return null;
|
|
470
474
|
}
|
|
471
475
|
const d = p.disableAutoMode;
|
|
@@ -18,7 +18,7 @@ import { projectApprovalsStreamLiveCapability } from "../../capabilities/approva
|
|
|
18
18
|
import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
|
|
19
19
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
20
20
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
21
|
-
import { coercePermissionModeStrict } from "../../task-settings.js";
|
|
21
|
+
import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS } from "../../task-settings.js";
|
|
22
22
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
23
23
|
function routePairingOf(deps, name, m) {
|
|
24
24
|
if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
|
|
@@ -52,13 +52,14 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
52
52
|
const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
|
|
53
53
|
const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
|
|
54
54
|
if (requestedModeRaw !== null && requestedMode === undefined) {
|
|
55
|
-
sendError(res, 400, "request.field_invalid",
|
|
55
|
+
sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
const entitlementSource = centerEntitlementSourceWired(deps.config);
|
|
59
59
|
const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), entitlementSource, localDeny: deps.config.permissionsDisableAutoMode });
|
|
60
60
|
if (autoModeFault !== undefined)
|
|
61
61
|
deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
|
|
62
|
+
const rulesLaneReachable = Boolean(deps.ruleConsent) && gatedPrincipal(req, deps.config) !== undefined;
|
|
62
63
|
sendJson(res, 200, {
|
|
63
64
|
...deps.capabilities,
|
|
64
65
|
asyncRuns: Boolean(deps.runStore),
|
|
@@ -91,7 +92,8 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
91
92
|
memoryConsolidationDriver: Boolean(deps.memoryConsolidation) && deps.config.operatorPrincipals.length > 0,
|
|
92
93
|
memoryOptOutGrant: Boolean(deps.memoryOptOutGrant) && deps.config.operatorPrincipals.length > 0,
|
|
93
94
|
outcomeLedger: Boolean(deps.outcomeSink?.summary) && (deps.config.requirePrincipal !== true || deps.config.operatorPrincipals.length > 0),
|
|
94
|
-
permissionRules:
|
|
95
|
+
permissionRules: rulesLaneReachable,
|
|
96
|
+
permissionRulesWrite: rulesLaneReachable,
|
|
95
97
|
permissionRulesRevoke: Boolean(deps.ruleConsent),
|
|
96
98
|
respondFreeFormRules: true,
|
|
97
99
|
respondBatchRuleOffers: true,
|
|
@@ -150,6 +152,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
150
152
|
sideQuery: true,
|
|
151
153
|
agentRoster: Boolean(deps.backgroundAgentStore),
|
|
152
154
|
retention: retentionAdvertisement(deps.config),
|
|
155
|
+
peerLane: Boolean(deps.peerDirectory),
|
|
153
156
|
sessionList: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
|
|
154
157
|
sessionSearch: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
|
|
155
158
|
sessionFork: Boolean(deps.sessionStorage?.fork),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref] 车二 + [ref] §2 —— **规则车道的 wire 面**([ref] §7 的导入半场 + [ref] 的撤销半场)。
|
|
3
3
|
*
|
|
4
|
+
* POST /v1/rules → 写一条**收紧**规则(deny/ask)进 durable 分区,单步、无票仪式
|
|
4
5
|
* POST /v1/rules/cc-import/prepare → 预览 + 一张**一次性、principal 绑定、有期、载荷绑定**的票
|
|
5
6
|
* POST /v1/rules/cc-import/redeem → 原子消费票 → confirm → 批量兑付进店
|
|
6
7
|
* GET /v1/rules → 列出自己名下**活着**的规则(keyset 分页,游标绑 rev)
|
|
@@ -11,6 +12,11 @@
|
|
|
11
12
|
* 因此**导入两口**只认已验明的 principal,连 `explicitOperatorOk` 的旁路都不给:一个 operator 想给
|
|
12
13
|
* 自己导规则,用他自己的 principal 走同一条路即可(那时他就是租户)。
|
|
13
14
|
*
|
|
15
|
+
* 🔴 **`POST /v1/rules` 是撤销口的对偶,不是导入口的简写**(S-511):它写的只有**收紧**方向
|
|
16
|
+
* (deny/ask),而收紧不需要同意仪式 —— 与撤销为什么不需要是同一条理由(core `removePersistedRule`
|
|
17
|
+
* 头注逐字:「a host may narrow on a user's behalf, it may not widen」)。放宽(allow)仍然只有两条路:
|
|
18
|
+
* 一张卡上的人点头,或一次显式的 settings 导入。方向表在 `permission-rule-vocab.ts`,不在本文件。
|
|
19
|
+
*
|
|
14
20
|
* 🔴 **撤销面两口多一条 operator 超范围域,而这不是上面那句话的例外,是它的对偶**([ref] §2 设计题
|
|
15
21
|
* 一裁定):`?principal=` / body 里的 `principal` 让 operator 读**或收回**任一租户的规则。方向决定授权:
|
|
16
22
|
* 收回是**收紧**(core 的 `removePersistedRule` 头注逐字:「a host may narrow on a user's behalf, it may
|
|
@@ -55,6 +61,11 @@ export declare const RULES_ROUTES: readonly [{
|
|
|
55
61
|
readonly id: "rules-list";
|
|
56
62
|
readonly path: "/v1/rules";
|
|
57
63
|
readonly methods: readonly ["GET"];
|
|
64
|
+
}, {
|
|
65
|
+
readonly id: "rules-write";
|
|
66
|
+
readonly path: "/v1/rules";
|
|
67
|
+
readonly credentialGated: readonly ["POST"];
|
|
68
|
+
readonly methods: readonly ["POST"];
|
|
58
69
|
}, {
|
|
59
70
|
readonly id: "rules-revoke";
|
|
60
71
|
readonly path: "/v1/rules";
|
|
@@ -3,11 +3,11 @@ import { sendJson, sendError, httpErrorCode } from "../send.js";
|
|
|
3
3
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
5
|
import { createLogger } from "../../observability/logger.js";
|
|
6
|
-
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
|
|
6
|
+
import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, sameWireRuleIdentity, serializeRuleScope } from "../../rules-consent.js";
|
|
7
7
|
import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
|
|
8
8
|
import { HttpError } from "../../security.js";
|
|
9
9
|
import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
|
|
10
|
-
import { PERSISTED_RULE_MATCHES, isPersistedRuleTool } from "../../permission-rule-vocab.js";
|
|
10
|
+
import { PERSISTED_RULE_MATCHES, isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
|
|
11
11
|
const logger = createLogger();
|
|
12
12
|
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
13
13
|
export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
@@ -58,7 +58,7 @@ const LOCAL_IMPORT_ROW_SHAPE_PIN = true;
|
|
|
58
58
|
void LOCAL_IMPORT_ROW_SHAPE_PIN;
|
|
59
59
|
const LocalImportPrepareBodySchema = z.object({ rows: z.array(LocalImportRowSchema).min(1) }).strict();
|
|
60
60
|
const LocalImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
|
|
61
|
-
const
|
|
61
|
+
const RuleIdentityBodySchema = z
|
|
62
62
|
.object({
|
|
63
63
|
behavior: z.enum(RULE_BEHAVIORS),
|
|
64
64
|
rule: z.string().min(1).max(1024),
|
|
@@ -118,6 +118,10 @@ export async function handleRules(req, res, match, ctx) {
|
|
|
118
118
|
await handleRuleRevoke(res, rawBody, principal, lane, deps.config.operatorPrincipals);
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
+
if (match.decl.id === "rules-write") {
|
|
122
|
+
await handleRuleWrite(res, rawBody, principal, lane, deps.config.operatorPrincipals);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
121
125
|
if (match.decl.id === "rules-local-import-prepare") {
|
|
122
126
|
const parsed = LocalImportPrepareBodySchema.safeParse(rawBody);
|
|
123
127
|
if (!parsed.success) {
|
|
@@ -242,8 +246,52 @@ async function handleRuleList(res, search, caller, lane, operatorPrincipals) {
|
|
|
242
246
|
: undefined;
|
|
243
247
|
sendJson(res, 200, { rules: page, rev: listing.rev, ...(nextCursor !== undefined ? { nextCursor } : {}) });
|
|
244
248
|
}
|
|
249
|
+
async function handleRuleWrite(res, rawBody, caller, lane, operatorPrincipals) {
|
|
250
|
+
const parsed = RuleIdentityBodySchema.safeParse(rawBody);
|
|
251
|
+
if (!parsed.success) {
|
|
252
|
+
sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const behavior = parsed.data.behavior;
|
|
256
|
+
if (!isDirectWritableRuleBehavior(behavior)) {
|
|
257
|
+
sendError(res, 400, "request.field_invalid", "this endpoint writes standing refusals only: behavior must be 'deny' or 'ask' — a standing approval is minted by answering a permission question or by importing settings, never by this call");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const scope = parseRuleScope(parsed.data.scope);
|
|
261
|
+
if (scope === undefined) {
|
|
262
|
+
sendError(res, 400, "request.body_shape", "scope must be 'global' or 'project:<root>' with a non-empty root");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const target = resolveTargetPrincipal(parsed.data.principal, caller, operatorPrincipals);
|
|
266
|
+
if (!target.ok) {
|
|
267
|
+
sendError(res, 403, "auth.operator_only", "writing another principal's rules is an operator-only action");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const written = await lane.writeRule({ principal: target.principal, behavior, rule: parsed.data.rule, scope });
|
|
271
|
+
switch (written.status) {
|
|
272
|
+
case "persisted":
|
|
273
|
+
case "no-op":
|
|
274
|
+
sendJson(res, 200, { status: written.status, rev: written.rev, rule: written.row });
|
|
275
|
+
return;
|
|
276
|
+
case "rejected":
|
|
277
|
+
logger.info("rule write refused", { reason: written.reason, detail: written.detail });
|
|
278
|
+
sendError(res, 400, "request.field_invalid", "this rule was not written — `reason` names which check refused it and `detail` says why", {
|
|
279
|
+
reason: written.reason,
|
|
280
|
+
detail: written.detail,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
case "indeterminate":
|
|
284
|
+
logger.info("rule write indeterminate", { detail: written.detail });
|
|
285
|
+
sendError(res, 503, "state.rule_write_failed", "this write could not be confirmed as a standing rule — retry, then reconcile with GET /v1/rules");
|
|
286
|
+
return;
|
|
287
|
+
default: {
|
|
288
|
+
const unknown = written;
|
|
289
|
+
throw new Error(`unknown rule write outcome: ${JSON.stringify(unknown)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
245
293
|
async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
|
|
246
|
-
const parsed =
|
|
294
|
+
const parsed = RuleIdentityBodySchema.safeParse(rawBody);
|
|
247
295
|
if (!parsed.success) {
|
|
248
296
|
sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
|
|
249
297
|
return;
|
|
@@ -264,7 +312,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
|
|
|
264
312
|
sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
|
|
265
313
|
return;
|
|
266
314
|
case "no-op": {
|
|
267
|
-
const stillLive = (await lane.listRules(target.principal)).rows.some((r) => r.rule
|
|
315
|
+
const stillLive = (await lane.listRules(target.principal)).rows.some((r) => sameWireRuleIdentity(r, { behavior: parsed.data.behavior, rule: parsed.data.rule, scope: serializeRuleScope(scope) }));
|
|
268
316
|
sendJson(res, 200, { status: "no-op", rev: outcome.rev, stillLive });
|
|
269
317
|
return;
|
|
270
318
|
}
|
|
@@ -280,6 +328,7 @@ export const RULES_ROUTES = [
|
|
|
280
328
|
{ id: "rules-local-import-prepare", path: RULES_LOCAL_IMPORT_PREPARE_PATH, methods: ["POST"] },
|
|
281
329
|
{ id: "rules-local-import-redeem", path: RULES_LOCAL_IMPORT_REDEEM_PATH, methods: ["POST"] },
|
|
282
330
|
{ id: "rules-list", path: RULES_PATH, methods: ["GET"] },
|
|
331
|
+
{ id: "rules-write", path: RULES_PATH, credentialGated: ["POST"], methods: ["POST"] },
|
|
283
332
|
{ id: "rules-revoke", path: RULES_PATH, credentialGated: ["DELETE"], methods: ["DELETE"] },
|
|
284
333
|
{ prefix: "/v1/rules/cc-import/", credentialGated: ["POST"] },
|
|
285
334
|
{ prefix: "/v1/rules/local-import/", credentialGated: ["POST"] },
|
|
@@ -13,6 +13,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
13
13
|
const SYNC_IMPORT_BATCH = 500;
|
|
14
14
|
import { SHA256_HEX_RE } from "../../digest-form.js";
|
|
15
15
|
import { assertNever } from "../route-ctx.js";
|
|
16
|
+
import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
|
|
16
17
|
export function createSessionSyncLocal() {
|
|
17
18
|
const pendingStagings = new Map();
|
|
18
19
|
const importLeases = new Map();
|
|
@@ -215,10 +216,15 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
215
216
|
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
216
217
|
return;
|
|
217
218
|
}
|
|
218
|
-
if (!Array.isArray(body.
|
|
219
|
+
if (!Array.isArray(body.anchors)) {
|
|
219
220
|
sendError(res, 400, "request.body_shape", "malformed import metadata");
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
223
|
+
const policyRead = readSessionRulesRecords(body.policy);
|
|
224
|
+
if (policyRead.problem !== undefined) {
|
|
225
|
+
sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
222
228
|
let fileHistory = null;
|
|
223
229
|
if (body.fileHistory !== undefined && body.fileHistory !== null) {
|
|
224
230
|
const door = validateFileHistoryExport(body.fileHistory);
|
|
@@ -235,7 +241,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
235
241
|
const resolution = body.resolution;
|
|
236
242
|
const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
|
|
237
243
|
const entryIds = body.entryIds;
|
|
238
|
-
const policy =
|
|
244
|
+
const policy = policyRead.records;
|
|
239
245
|
const anchors = body.anchors;
|
|
240
246
|
const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
|
|
241
247
|
const relA = classifySyncRelationshipByIds(entryIds, dstIds);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isUuidV7, legacyUnownedAmnestyOpen } from "../../security.js";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
import { filterFingerprint } from "../cursor-fingerprint.js";
|
|
4
|
+
import { sessionTempoOfRunStatus } from "../../plugins/store-contracts.js";
|
|
4
5
|
import { assertNever } from "../route-ctx.js";
|
|
5
6
|
export async function handleSessionsList(req, res, match, ctx) {
|
|
6
7
|
const { deps } = ctx;
|
|
@@ -120,7 +121,11 @@ async function handleSessionList(res, lister, query, forceOwner, includeUnowned
|
|
|
120
121
|
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
121
122
|
const last = page[page.length - 1];
|
|
122
123
|
const nextCursor = hasMore && last ? encodeSessionCursor({ lastActivityAt: last.lastActivityAt, sessionId: last.sessionId }, f) : undefined;
|
|
123
|
-
|
|
124
|
+
const sessions = page.map((r) => {
|
|
125
|
+
const tempo = sessionTempoOfRunStatus(r.lastStatus);
|
|
126
|
+
return tempo === undefined ? r : { ...r, tempo };
|
|
127
|
+
});
|
|
128
|
+
sendJson(res, 200, { sessions, ...(nextCursor ? { nextCursor } : {}) });
|
|
124
129
|
}
|
|
125
130
|
export const SESSIONS_LIST_ROUTES = [
|
|
126
131
|
{ id: "sessions-list", path: "/v1/sessions", methods: ["GET"] },
|
|
@@ -5,6 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
7
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
8
|
+
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
8
9
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
9
10
|
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
|
|
10
11
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -741,25 +742,26 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
741
742
|
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
742
743
|
return;
|
|
743
744
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
745
|
+
const read = readSessionPermissionRules(body.rules);
|
|
746
|
+
if (read.problem !== undefined) {
|
|
747
|
+
switch (read.problem.kind) {
|
|
748
|
+
case "not-an-object":
|
|
749
|
+
sendError(res, 400, "request.body_shape", describeSessionPermissionRulesProblem(read.problem));
|
|
750
|
+
return;
|
|
751
|
+
case "field-not-string-array":
|
|
752
|
+
sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
|
|
753
|
+
return;
|
|
754
|
+
default: {
|
|
755
|
+
const unhandled = read.problem;
|
|
756
|
+
throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
747
759
|
}
|
|
748
760
|
if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
|
|
749
761
|
sendError(res, 400, "request.field_invalid", "expectedRev must be a non-negative integer");
|
|
750
762
|
return;
|
|
751
763
|
}
|
|
752
|
-
const
|
|
753
|
-
const rules = {};
|
|
754
|
-
for (const f of ["toolAllow", "toolDeny", "allowDirs", "commandAllow", "commandDeny"]) {
|
|
755
|
-
if (raw[f] === undefined)
|
|
756
|
-
continue;
|
|
757
|
-
if (!Array.isArray(raw[f]) || !raw[f].every((x) => typeof x === "string")) {
|
|
758
|
-
sendError(res, 400, "request.field_invalid", `${f} must be an array of strings`);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
rules[f] = raw[f];
|
|
762
|
-
}
|
|
764
|
+
const rules = read.rules;
|
|
763
765
|
try {
|
|
764
766
|
const stored = await store.putRules(sessionId, target, rules, { ...(typeof body.expectedRev === "number" ? { expectedRev: body.expectedRev } : {}), operator });
|
|
765
767
|
sendJson(res, 200, { rules: stored });
|
package/dist/http/server.d.ts
CHANGED
|
@@ -163,6 +163,18 @@ export interface ServiceStoreDeps {
|
|
|
163
163
|
* `task_run` runs ledger. When it carries `listSessions`, GET /v1/sessions sources sessions from the session
|
|
164
164
|
* store (task_run only joins for run preview); `fork`/`deleteSession` enable the fork/delete routes. */
|
|
165
165
|
sessionStorage?: OwnerAwareSessionStore;
|
|
166
|
+
/**
|
|
167
|
+
* [ref] —— 跨会话车道的**发现席**(core `RunnerDeps.peerDirectory`),与 `RunnerDeps` 侧**同一个对象**
|
|
168
|
+
* (`boot/stage-10-http-server.ts` 直接递 `runnerDeps.peerDirectory`)。
|
|
169
|
+
*
|
|
170
|
+
* 🔴 HTTP 面**今天不读它的内容**:它在这里的唯一作用是让 `GET /v1/capabilities.peerLane` 那一位
|
|
171
|
+
* 「说 yes ⟺ 车道真的挂上了」成为**同一个值**的读取,而不是把那条三合取的装配谓词在路由里**重写一遍**
|
|
172
|
+
* (重写的那一份必然漂 —— 本仓能力位族的既有病,见 `routes/capabilities.ts` 里 `agentRoster` 的同款注)。
|
|
173
|
+
* ⚠️ 精确说法是**值** iff,不是键 present-iff:那一位与同族的 `sessionList` 一样是 `Boolean(...)`,
|
|
174
|
+
* 本席缺席时键仍在、值为 `false`(`test/capabilities-http.test.ts` 的桩部署那一格逐字钉着它)。
|
|
175
|
+
* 「键缺席」只发生在**老版本 worker** 上,那是版本事实,不是本部署的车道事实。
|
|
176
|
+
*/
|
|
177
|
+
peerDirectory?: import("@sema-agent/core").PeerDirectory;
|
|
166
178
|
/** S8 workflow-run store ([ref]/98): enables GET /v1/workflows (list) + /v1/workflows/:id (detail).
|
|
167
179
|
* Set when SELF_ORCHESTRATION_ENABLED. Live progress (/v1/workflows/:id/stream) is in-process
|
|
168
180
|
* (subscribeWorkflow) and works whenever self-orchestration is on, store or not. Runs are owner-gated by
|
|
@@ -264,6 +264,18 @@ export interface TaskRequestBody {
|
|
|
264
264
|
* it takes the same single-user gate as backgroundShell/hooks (multi-tenant warns + ignores; advertised via
|
|
265
265
|
* capabilities.retainBackgroundProcesses). Boolean fail-loud at submit; only `true` rides the spec. */
|
|
266
266
|
retainBackgroundProcesses?: boolean;
|
|
267
|
+
/** S-493(core 7.8.0 [ref] `TaskSpec.excludeAllTools`,cli L-207 / 板 [ref]): **整只工具面卸载**
|
|
268
|
+
* —— 调用方工具、脚手架、手、MCP、skills 一个都不挂,模型这一 run 里没有任何工具可调、终局是纯文本。
|
|
269
|
+
*
|
|
270
|
+
* 🔴 **与 {@link excludeTools} 是两种语义,刻意不折叠**:`"*"` 是一个合法的调用方工具名,所以「卸全部」
|
|
271
|
+
* 不能写成一条通配规则(那会把「卸全部」与「卸一只叫 `*` 的工具」塌成一件事)。两键同带 ⇒ 各自生效
|
|
272
|
+
* (全卸之后名单里那几只本来也不在,不矛盾、不拒)。
|
|
273
|
+
* 只 literal `true` 上 spec(`false` / 缺席 = 名册照旧,不挂键保持 byte-compat —— 挂一个 `false` 上去
|
|
274
|
+
* 等于替引擎重申了一次它自己的缺省);非 boolean 由 HTTP 门 400 `request.field_invalid`,resume 重放
|
|
275
|
+
* defensive DROP。per-run 工具面**收窄**无跨租户面 ⇒ 无租户门(`excludeTools` 同姿势)。
|
|
276
|
+
* 探测:本键在不在受理面由**键闭集**自己回答 —— 没接线的 server 版本对它 400 `request.body_shape`
|
|
277
|
+
* 并逐字列名,所以**不加能力位**(一个键一条探测路,不长第二条)。 */
|
|
278
|
+
excludeAllTools?: boolean;
|
|
267
279
|
/** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
|
|
268
280
|
* tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
|
|
269
281
|
excludeTools?: string[];
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "./observability/logger.js";
|
|
2
|
+
import { buildFatalErrorText } from "./boot/fatal-exit.js";
|
|
2
3
|
import { runStage01 } from "./boot/stage-01-config.js";
|
|
3
4
|
import { runStage02 } from "./boot/stage-02-registry-pull.js";
|
|
4
5
|
import { runStage03 } from "./boot/stage-03-store-backend.js";
|
|
@@ -22,7 +23,7 @@ async function main() {
|
|
|
22
23
|
await runStage10(s09);
|
|
23
24
|
}
|
|
24
25
|
void main().catch((err) => {
|
|
25
|
-
createLogger().error("fatal", { err:
|
|
26
|
+
createLogger().error("fatal", { err: buildFatalErrorText(err) });
|
|
26
27
|
process.exit(1);
|
|
27
28
|
});
|
|
28
29
|
//# sourceMappingURL=main.js.map
|
|
@@ -3,6 +3,14 @@ export declare const GATEWAY_URL_USERINFO_CODE = "config.gateway_url_userinfo";
|
|
|
3
3
|
/** 同族第二码(S-483 座⑨):这一位根本不是一个层级式绝对 URL。三根键共用一码,与上面那只同纪律
|
|
4
4
|
* (码按**规则**命名,不按键名 —— 键名已经逐字在拒因文案里点过了)。 */
|
|
5
5
|
export declare const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_invalid";
|
|
6
|
+
/**
|
|
7
|
+
* 🔴 **第三条臂的机读码(S-514;core 7.24.2 / [ref] 提货)——请求根带 `#fragment`**。
|
|
8
|
+
*
|
|
9
|
+
* 为什么与前两条各自一码:运维要按码行动,而三条臂的**出路不同**(补全 URL / 把凭据搬去旋钮 / 删掉 `#`
|
|
10
|
+
* 之后的一切)。`fetch` **从不把 fragment 发上线**,所以这一形此前是**静默**打根自己的路径、端点后缀整段
|
|
11
|
+
* 落进被丢弃的那一半 —— 请求发得出去、也可能 200,只是打错了地方而一个字不说。
|
|
12
|
+
*/
|
|
13
|
+
export declare const MODEL_ROUTE_URL_FRAGMENT_CODE = "config.model_route_url_fragment";
|
|
6
14
|
/**
|
|
7
15
|
* 模型路由 base URL 的拒因(`undefined` = 这一位合法)—— **这三根键的唯一判官**。
|
|
8
16
|
*
|