@sema-agent/server 7.115.0 → 7.116.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/MIGRATION.md +167 -4
- package/USAGE.md +6 -3
- package/dist/approval-ask-audit-store.d.ts +25 -6
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/approval-card.d.ts +8 -4
- package/dist/approval-card.js +2 -0
- package/dist/approval-crash-convergence.d.ts +60 -0
- package/dist/approval-crash-convergence.js +75 -0
- package/dist/approval-deny-reasons.d.ts +13 -0
- package/dist/approval-deny-reasons.js +2 -0
- package/dist/approval.js +2 -1
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/hands-lane.d.ts +0 -23
- package/dist/capabilities/hands-lane.js +3 -2
- package/dist/capabilities/send-user-file-tool.d.ts +10 -22
- package/dist/capabilities/send-user-file-tool.js +1 -8
- package/dist/capability-facts.d.ts +296 -0
- package/dist/capability-facts.js +354 -0
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +16 -13
- package/dist/config.js +12 -12
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +22 -5
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +10 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.js +14 -21
- package/dist/http/routes/admin-config-refresh.js +7 -2
- package/dist/http/routes/admin-drain.js +7 -2
- package/dist/http/routes/admin-memory-optout.js +20 -6
- package/dist/http/routes/adoption.js +19 -5
- package/dist/http/routes/agents-roster.js +4 -2
- package/dist/http/routes/approvals-assistant.js +41 -18
- package/dist/http/routes/background.d.ts +0 -2
- package/dist/http/routes/background.js +4 -5
- package/dist/http/routes/capabilities.d.ts +2 -1
- package/dist/http/routes/capabilities.js +62 -65
- package/dist/http/routes/config-catalog.d.ts +4 -5
- package/dist/http/routes/config-catalog.js +16 -2
- package/dist/http/routes/devices.js +17 -11
- package/dist/http/routes/diagnostics.js +6 -1
- package/dist/http/routes/fleet.js +4 -2
- package/dist/http/routes/images.js +30 -3
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/mcp-reconnect.js +17 -10
- package/dist/http/routes/memory-bundle.js +20 -6
- package/dist/http/routes/memory-compliance.js +19 -5
- package/dist/http/routes/memory-consolidation.d.ts +9 -12
- package/dist/http/routes/memory-consolidation.js +17 -3
- package/dist/http/routes/memory-origin.js +19 -5
- package/dist/http/routes/notify-wake.js +5 -3
- package/dist/http/routes/observability.js +19 -5
- package/dist/http/routes/retention-ops.js +6 -1
- package/dist/http/routes/rules.js +49 -21
- package/dist/http/routes/run-memory-capture.js +5 -2
- package/dist/http/routes/runs.js +90 -60
- package/dist/http/routes/session-sync.js +64 -53
- package/dist/http/routes/sessions-list.js +8 -5
- package/dist/http/routes/sessions.js +39 -24
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/routes/trace-usage.js +18 -11
- package/dist/http/routes/workflows.js +36 -13
- package/dist/http/server.js +4 -1
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/namespaced-rule-name.d.ts +54 -0
- package/dist/namespaced-rule-name.js +33 -0
- package/dist/observability/corrupt-read-seat.d.ts +4 -3
- package/dist/observability/corrupt-read-seat.js +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/plugins/file-run-store.d.ts +7 -1
- package/dist/plugins/file-run-store.js +20 -7
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/one-time-migrations.d.ts +4 -2
- package/dist/plugins/one-time-migrations.js +26 -3
- package/dist/plugins/pg-session-storage.js +1 -1
- package/dist/plugins/run-store-sql.js +4 -4
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/store-backend.d.ts +6 -1
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/store-contracts.d.ts +27 -8
- package/dist/plugins/store-contracts.js +12 -3
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +37 -3
- package/dist/run-local.js +48 -5
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/session-policy-wire.d.ts +40 -60
- package/dist/session-policy-wire.js +60 -32
- package/dist/spec-fields.d.ts +32 -0
- package/dist/spec-fields.js +31 -16
- package/dist/standing-answer-grant.d.ts +8 -0
- package/dist/standing-answer-grant.js +5 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/tool-approval.d.ts +16 -0
- package/dist/tool-approval.js +29 -7
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/self-minted-wire.d.ts +42 -0
- package/dist/trace/self-minted-wire.js +13 -0
- package/dist/user-input-ledger.d.ts +8 -5
- package/package.json +5 -3
- package/scripts/drain-check-local.mjs +115 -25
- package/scripts/drain-check.mysql.sql +67 -0
- package/scripts/drain-check.pg.sql +71 -0
|
@@ -481,7 +481,7 @@ function stageApprovalRequire(raw) {
|
|
|
481
481
|
return {
|
|
482
482
|
ok: false,
|
|
483
483
|
detail: unmatchable.message,
|
|
484
|
-
reason: `published governance.approvalRequire has ${unmatchable.names.length} entr${unmatchable.names.length === 1 ? "y" : "ies"} that match no live tool (retired name / incomplete
|
|
484
|
+
reason: `published governance.approvalRequire has ${unmatchable.names.length} entr${unmatchable.names.length === 1 ? "y" : "ies"} that match no live tool (retired name / incomplete protocol name / server segment the engine never mints / CC-form rule) — the offending entries are named in the sema_registry_approval_require_invalid log line`,
|
|
485
485
|
names: unmatchable.names,
|
|
486
486
|
kept: KEPT_WOULD_MATCH_NOTHING,
|
|
487
487
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -293,11 +293,14 @@ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCen
|
|
|
293
293
|
* 名单比的是**整串工具名**,带括号那串不是任何活工具的名字 ⇒ 永不匹配。指引=按命令名走
|
|
294
294
|
* `runtime.commandPolicy`,整条 shell 进门走 `MANUAL_MODE_SHELL_GATE=always`。判别式见
|
|
295
295
|
* {@link findCcRuleFormNames}(它同时是 `allow` 腿的**唯一**判据,见下方例外说明)。
|
|
296
|
-
* - 退役名(`bash`/`Task`/`KillShell`…)
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
296
|
+
* - 退役名(`bash`/`Task`/`KillShell`…)与 pre-prefix 短名(`figma__x`):S-1047(core 7.41.0 #1156)起判据是 core 导出的
|
|
297
|
+
* **那一只** `judgeRuleToolName`(准备阶段、两只会话规则店、本仓会话规则写口同调),本函数零手写臂;指引 = core 表给的现役指引 /
|
|
298
|
+
* 补全协议前缀。
|
|
299
|
+
* - 够不着挂载工具的协议名:缺段(`mcp__`、`mcp__srv__`、`mcp____x`)与**对端段铸不出**(`mcp__github.com`、`mcp__github.com__*`、
|
|
300
|
+
* 超长 / 归一后带连串下划线的服务器段;服务器段带通配的 `mcp__*` 单列判词 `wildcard_peer`)—— 前缀对,但引擎铸注册名时归一对端段,而规则对对端段逐字比,永不匹配。
|
|
301
|
+
* 判官与句子是 `namespaced-rule-name.ts` 的那**一只**(判据 = core `mintNamespacePrefix` 的自铸对照;会话规则写口两腿与 SQL 店写口同调),
|
|
302
|
+
* 本文件零手写臂。修前(7.116.0 之前)这里只有缺段一臂外加一条只排 `*` 的特例(`coversAServer`),对端段带点号 / 超长的覆盖形在每个面都照收。
|
|
303
|
+
* ⚠️ 两段覆盖形 `mcp__<server>`(服务器段是自铸形)够得着(S-1047 收掉的分叉,见下)。
|
|
301
304
|
* - **core 的文法编不出来的串**(名字里带反斜杠、单条 >512 字符、空串):判词取自 core 自己的
|
|
302
305
|
* `validatePermissionRules`,见 {@link uncompilableRuleGuidance}。[ref]①([ref])补齐 ——
|
|
303
306
|
* 此前只有 `ask` 腿(经 `createPermissionRulePolicy` 的 throw)拒得出这一族,`deny` 静默 no-op。
|
|
@@ -313,12 +316,12 @@ export declare function isRosterGateArmed(configCenter: ServiceConfig["configCen
|
|
|
313
316
|
* `<ns>__<server>__*` / `<ns>__<server>__<glob>` 在**每一条**腿上都是**真判**,不再是
|
|
314
317
|
* 「放行进 no-op」—— 而那正是 [ref] 病形(`APPROVAL_DENY=mcp__figma__*` 判 allow)的根。
|
|
315
318
|
*
|
|
316
|
-
*
|
|
317
|
-
* (`
|
|
318
|
-
*
|
|
319
|
-
* (
|
|
320
|
-
*
|
|
321
|
-
*
|
|
319
|
+
* 🔴 **S-1047(core 7.41.0 #1156)收掉的分叉:两段覆盖形 `<ns>__<server>`(`mcp__figma`)自 7.116.0 起受理**。core 全家把它解成
|
|
320
|
+
* covering(这台服务器的每一只工具,与 `mcp__figma__*` 同义),core 的判词 `judgeRuleToolName` 判它 `current`,本仓会话规则写口也一直
|
|
321
|
+
* 收它;而本函数此前以「一名两义」(运维也可能是把三段名写漏了一段)拒它,句子却说它「永远不生效」—— 对它不成立,且同一个名字在本仓两张表上
|
|
322
|
+
* 两种答案。收成一个答案:受理。代价(三问在 CHANGELOG 7.116.0):少了「三段名漏打一截」的那一次拦截 —— 想要一只工具就写全名;
|
|
323
|
+
* 拒放集合的变化逐条在 MIGRATION 7.116.0 节。服务器段带通配(`mcp__*`)仍拒(core 的匹配器对服务器段逐字比,一只都够不着;7.116.0 起它是
|
|
324
|
+
* 「对端段铸不出」的一例,不再单列)。
|
|
322
325
|
*/
|
|
323
326
|
export interface UnmatchableToolName {
|
|
324
327
|
name: string;
|
|
@@ -354,7 +357,7 @@ export interface UnmatchableToolNameJudgment {
|
|
|
354
357
|
* (`formatUnmatchableToolNames`)是两只导出,句子里那半句(「这些表都是整串逐字比对」)只对名表成立 —— 规则编译器的
|
|
355
358
|
* 三只桶(与 `APPROVAL_GRANT`)是按规则文法编、按 covering 匹配的,不是整串比对。
|
|
356
359
|
*
|
|
357
|
-
* 句子对两种消费者、三种态度都成立:每一条要么永远匹配不到活工具(退役名 / 无协议前缀的短名 /
|
|
360
|
+
* 句子对两种消费者、三种态度都成立:每一条要么永远匹配不到活工具(退役名 / 无协议前缀的短名 / 缺段或对端段铸不出的协议名 / 名表里的内容形),要么编不成
|
|
358
361
|
* 一条规则(core 文法的判词原样带出);名字按写的样子读,不做别名折叠、不补协议前缀。逐条的「为什么 / 怎么改」在 `→` 之后。
|
|
359
362
|
*/
|
|
360
363
|
export declare function unmatchableToolNamesRefusal(source: string, names: readonly string[], judgment: UnmatchableToolNameJudgment): {
|
|
@@ -364,7 +367,7 @@ export declare function unmatchableToolNamesRefusal(source: string, names: reado
|
|
|
364
367
|
/**
|
|
365
368
|
* 7.115.0 —— 运维 allow 规则 `APPROVAL_GRANT` 的**拒启判据与拒因句**(同一只函数;`config-invariants.ts` 的 `…Refusal` 形:
|
|
366
369
|
* 返回句子 = 拒,`undefined` = 通过)。判据与请求腿的 `settings.permissions.allow` 是**同一只**:
|
|
367
|
-
* ① 名字形与 core 文法 —— {@link unmatchableToolNamesRefusal},消费者 `rule-policy`、behavior `allow`(退役名 / 无前缀短名 /
|
|
370
|
+
* ① 名字形与 core 文法 —— {@link unmatchableToolNamesRefusal},消费者 `rule-policy`、behavior `allow`(退役名 / 无前缀短名 / 缺段或对端段铸不出的协议名 /
|
|
368
371
|
* 反斜杠 / 超长 / 内容形与参数级的 allow;句子点名哪一条、为什么、怎么改);
|
|
369
372
|
* ② 整表试编一次 —— 编的就是治理折叠里那一只(`runtime-governance.ts` {@link compileApprovalGrantLayers}),于是 core 的整表帽这类
|
|
370
373
|
* 只在编译时才抛的判据也在启动时响亮,而不是每个任务一次 500。
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE,
|
|
6
|
+
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE, RECOMMENDED_SENSITIVE_PATTERNS, judgeRuleToolName, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable } from "@sema-agent/core";
|
|
7
7
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES, CENTER_CREDENTIAL_ENV } from "@sema-agent/settings-schema";
|
|
8
8
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
9
9
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
@@ -18,6 +18,7 @@ import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-post
|
|
|
18
18
|
import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
|
|
19
19
|
import {} from "./posture-source.js";
|
|
20
20
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
21
|
+
import { describeUnreachableNamespacedRuleName, ruleEntryToolName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
|
|
21
22
|
import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
|
|
22
23
|
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
|
|
23
24
|
import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
|
|
@@ -1245,29 +1246,28 @@ function findUnmatchableToolNames(names, opts) {
|
|
|
1245
1246
|
continue;
|
|
1246
1247
|
}
|
|
1247
1248
|
}
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1249
|
+
const subject = opts.consumer === "tool-name-table" ? name : ruleEntryToolName(name);
|
|
1250
|
+
const verdict = judgeRuleToolName(subject, NO_DECLARED_TOOL_NAMES);
|
|
1251
|
+
if (verdict.kind === "retired") {
|
|
1252
|
+
out.push({ name, guidance: verdict.guidance });
|
|
1251
1253
|
continue;
|
|
1252
1254
|
}
|
|
1253
1255
|
const uncompilable = uncompilableRuleGuidance(name, opts.behavior, opts.pathBases);
|
|
1254
|
-
const subject = opts.consumer === "tool-name-table" ? name : (() => { const parsed = parseRuleText(name, opts.behavior); return "rule" in parsed ? parsed.rule.tool : name; })();
|
|
1255
1256
|
if (uncompilable !== undefined) {
|
|
1256
1257
|
out.push({ name, guidance: uncompilable });
|
|
1257
1258
|
continue;
|
|
1258
1259
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (subject.includes("__"))
|
|
1262
|
-
out.push({ name, guidance: `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentToolName(name)}`).join(" or ")}` });
|
|
1260
|
+
if (verdict.kind === "unprefixed_namespace") {
|
|
1261
|
+
out.push({ name, guidance: `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentToolName(name)}`).join(" or ")}` });
|
|
1263
1262
|
continue;
|
|
1264
1263
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1264
|
+
const unreachable = unreachableNamespacedRuleNameOf(subject);
|
|
1265
|
+
if (unreachable !== undefined)
|
|
1266
|
+
out.push({ name, guidance: describeUnreachableNamespacedRuleName(unreachable) });
|
|
1268
1267
|
}
|
|
1269
1268
|
return out;
|
|
1270
1269
|
}
|
|
1270
|
+
const NO_DECLARED_TOOL_NAMES = new Set();
|
|
1271
1271
|
export function unmatchableToolNamesRefusal(source, names, judgment) {
|
|
1272
1272
|
const bad = findUnmatchableToolNames(names, judgment);
|
|
1273
1273
|
if (bad.length === 0)
|
package/dist/http/admission.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
|
52
52
|
* 🔴 **server 侧禁裁列表 / 禁截描述**:渲染归引擎(单一属主)。本函数只判「这一条技能的形能不能用」。
|
|
53
53
|
*/
|
|
54
54
|
export declare function validateUserSkills(skills: unknown): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* core 7.41.0 #1303 —— 顶层技能带 `manifest` 的拒因句(单铸点)。出路只点本服务上**真存在**的旋钮:技能的两只清单
|
|
57
|
+
* (`allowedTools` / `disallowedTools`)、圈工具 = deny 规则(请求 `settings.permissions.deny` / 运维 `APPROVAL_DENY`)或子代定义的
|
|
58
|
+
* `agents[].allowTools`、圈写路径 = 会话规则 `allowDirs`(`PUT /v1/sessions/:id/policy`)。
|
|
59
|
+
*/
|
|
60
|
+
export declare const SKILL_MANIFEST_RETIRED = "skill.manifest was retired in core 7.41.0 \u2014 a skill's tools are its allowedTools / disallowedTools now (an allow-rule and a deny-rule source for the run the skill is activated in, never a fence); move manifest.allowTools to allowedTools. To fence tools use deny rules (settings.permissions.deny) or a delegated agent's allowTools; to fence write paths use the session rules' allowDirs";
|
|
55
61
|
export declare function createAdmission(ctx: {
|
|
56
62
|
deps: FlatServiceDeps;
|
|
57
63
|
runnerFor: FlatServiceDeps["runnerFor"];
|
package/dist/http/admission.js
CHANGED
|
@@ -5,13 +5,13 @@ import { HttpError } 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, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
6
6
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
7
7
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
8
|
-
import
|
|
9
|
-
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
8
|
+
import * as facts from "../capability-facts.js";
|
|
10
9
|
import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
|
|
11
10
|
import { judgeRequestWebSearch } from "../plugins/web-search.js";
|
|
12
|
-
import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
11
|
+
import { describeUnreachableSkillToolListEntry, normalizeApproachNotice, skillToolListOk, unknownTaskAgentModelRef, unreachableSkillToolListEntryOf, validateTaskAgents } from "../spec-fields.js";
|
|
13
12
|
import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
|
|
14
13
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
14
|
+
import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
|
|
15
15
|
import { resolveDevicePlacementIntent } from "./device-placement.js";
|
|
16
16
|
import { sendError, httpErrorCode } from "./send.js";
|
|
17
17
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
@@ -33,9 +33,19 @@ export function validateUserSkills(skills) {
|
|
|
33
33
|
return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
|
|
34
34
|
if (s["baseDir"] !== undefined && !isValidCwd(s["baseDir"]))
|
|
35
35
|
return `skill.baseDir must be an absolute path string of at most ${MAX_CWD_CHARS} characters with no ".." segment and no NUL byte (it is not resolved or read here — the engine only substitutes it into the skill's own text)`;
|
|
36
|
+
if (skillCarriesRetiredManifest(s))
|
|
37
|
+
return SKILL_MANIFEST_RETIRED;
|
|
38
|
+
for (const key of ["allowedTools", "disallowedTools"]) {
|
|
39
|
+
if (s[key] !== undefined && !skillToolListOk(s[key]))
|
|
40
|
+
return `skill.${key} must be an array of strings (permission-rule spellings: a tool name, mcp__<server> / mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**))`;
|
|
41
|
+
}
|
|
42
|
+
const unreachable = unreachableSkillToolListEntryOf(s);
|
|
43
|
+
if (unreachable !== undefined)
|
|
44
|
+
return describeUnreachableSkillToolListEntry(unreachable);
|
|
36
45
|
}
|
|
37
46
|
return null;
|
|
38
47
|
}
|
|
48
|
+
export const SKILL_MANIFEST_RETIRED = "skill.manifest was retired in core 7.41.0 — a skill's tools are its allowedTools / disallowedTools now (an allow-rule and a deny-rule source for the run the skill is activated in, never a fence); move manifest.allowTools to allowedTools. To fence tools use deny rules (settings.permissions.deny) or a delegated agent's allowTools; to fence write paths use the session rules' allowDirs";
|
|
39
49
|
const MAX_IMAGES_PER_REQUEST = 20;
|
|
40
50
|
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
41
51
|
function mentionableNames(config, catalog) {
|
|
@@ -361,10 +371,10 @@ export function createAdmission(ctx) {
|
|
|
361
371
|
return null;
|
|
362
372
|
}
|
|
363
373
|
{
|
|
364
|
-
const
|
|
374
|
+
const admission = facts.selfOrchestrationAdmission(deps);
|
|
365
375
|
const wantsSelfOrchestration = body.selfOrchestration === true
|
|
366
376
|
|| parseTaskSettings(body.settings).settings?.ultracode === true;
|
|
367
|
-
if (
|
|
377
|
+
if (!admission.available && wantsSelfOrchestration) {
|
|
368
378
|
sendError(res, 501, "capability.self_orchestration_required", "self-orchestration is enabled on this worker (SELF_ORCHESTRATION_ENABLED) but it is multi-tenant (REQUIRE_PRINCIPAL) and wires NO per-principal entitlement source, so the engine's allowWorkflows gate — which is tighten-only, meaning an absent resolver leaves it UNGATED — is fail-closed for every principal: no request can ever get the workflow engine here (GET /v1/capabilities reports workflows:false with workflowsGate.denial \"entitlement_resolver_absent\"). Wire a config center (SEMA_REGISTRY_URL + token, not dry-run) to gate it per principal, or set SELF_ORCHESTRATION_ENABLED=false — the honest posture for a worker that cannot grant it. To submit this task without the workflow engine, drop selfOrchestration / settings.ultracode; ultracode's other half (the max thinking tier) is available as reasoningEffort: \"max\"");
|
|
369
379
|
return null;
|
|
370
380
|
}
|
|
@@ -478,6 +488,13 @@ export function createAdmission(ctx) {
|
|
|
478
488
|
sendError(res, 400, "request.field_conflict", "disableSkills: true means this run installs no skill at all (not the deployment's, not the request's), so it cannot come with a non-empty skills or agents[].skills — send one or the other");
|
|
479
489
|
return null;
|
|
480
490
|
}
|
|
491
|
+
{
|
|
492
|
+
const v = body.userInvokedSkills;
|
|
493
|
+
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
494
|
+
sendError(res, 400, "request.field_invalid", "userInvokedSkills must be an array of non-empty skill-name strings (each one a skill this run carries)");
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
481
498
|
for (const key of ["excludeTools", "deferTools"]) {
|
|
482
499
|
const v = body[key];
|
|
483
500
|
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
@@ -92,6 +92,9 @@ export declare function runOwnerReadable(principal: string | undefined, owner: s
|
|
|
92
92
|
* and two more hand-inlined copies sat in approvals-assistant — a future hardening of this judgment would have
|
|
93
93
|
* missed them. All call sites now consume THIS function; do not re-introduce a sibling. */
|
|
94
94
|
export declare function explicitOperatorOk(principal: string | undefined, operatorPrincipals: readonly string[]): boolean;
|
|
95
|
+
/** operator 面族在「名单为空」这一形上的**唯一**拒发口:事实 `operatorLane`(`capability-facts.ts`)为假 ⇒ 这一面对任何
|
|
96
|
+
* 身份都打不通 = 这台部署上不可用 ⇒ 501,不是鉴权拒(403 只留给「名单非空而你不在名单上」)。能力位读的是同一只事实。 */
|
|
97
|
+
export declare function sendOperatorPrincipalsRequired(res: ServerResponse): void;
|
|
95
98
|
/** May `principal` act as an OPERATOR on the F4 approval queue? Empty `operatorPrincipals` = legacy
|
|
96
99
|
* behavior (the shared service token IS the operator boundary → everyone authenticated is an operator).
|
|
97
100
|
* Otherwise only listed principals are operators. */
|
|
@@ -95,6 +95,9 @@ export function runOwnerReadable(principal, owner, config) {
|
|
|
95
95
|
export function explicitOperatorOk(principal, operatorPrincipals) {
|
|
96
96
|
return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
|
|
97
97
|
}
|
|
98
|
+
export function sendOperatorPrincipalsRequired(res) {
|
|
99
|
+
sendError(res, 501, "capability.operator_principals_required", "this endpoint is operator-only and OPERATOR_PRINCIPALS is empty, so no caller can be an operator on this deployment; configure OPERATOR_PRINCIPALS to enable it");
|
|
100
|
+
}
|
|
98
101
|
export function isOperator(principal, operatorPrincipals) {
|
|
99
102
|
if (operatorPrincipals.length === 0)
|
|
100
103
|
return true;
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -66,6 +66,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
66
66
|
read_face_damaged: false,
|
|
67
67
|
read_face_forged: false,
|
|
68
68
|
write_face_damaged: false,
|
|
69
|
+
skill_grants_damaged: false,
|
|
69
70
|
constraint_chain_missing: false,
|
|
70
71
|
};
|
|
71
72
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
@@ -121,13 +122,21 @@ export function checkpointErrorDisposition(e) {
|
|
|
121
122
|
const CHECKPOINT_ERROR_FIELD_ROLE = {
|
|
122
123
|
boundCallId: "binding",
|
|
123
124
|
boundInputHash: "binding",
|
|
125
|
+
boundGrantDigest: "regrant",
|
|
124
126
|
answer: "answer",
|
|
125
127
|
hostDecision: "outcome_cell",
|
|
126
128
|
approver: "outcome_cell",
|
|
127
129
|
taskConfig: "config",
|
|
128
130
|
inheritedGate: "config",
|
|
129
131
|
};
|
|
130
|
-
const
|
|
132
|
+
const ROLE_MINTS_RESUME_OUTCOME_INVALID = {
|
|
133
|
+
binding: false,
|
|
134
|
+
answer: false,
|
|
135
|
+
outcome_cell: true,
|
|
136
|
+
regrant: true,
|
|
137
|
+
config: false,
|
|
138
|
+
};
|
|
139
|
+
const NON_BINDING_INVALID_OUTCOME_FIELDS = new Set(Object.keys(CHECKPOINT_ERROR_FIELD_ROLE).filter((f) => ROLE_MINTS_RESUME_OUTCOME_INVALID[CHECKPOINT_ERROR_FIELD_ROLE[f]]));
|
|
131
140
|
function notResumableAnswer() {
|
|
132
141
|
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
133
142
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能上已退役的 `manifest` 键 —— 「一条技能还带它」的**唯一判据**(core 7.41.0 起 `SkillSpec.manifest` 退役,零别名)。
|
|
3
|
+
*
|
|
4
|
+
* 两个读者,同一句话:
|
|
5
|
+
* · 提交口的顶层技能门(`admission.ts` 的 `validateUserSkills`):带它(任何值)⇒ 点名拒;
|
|
6
|
+
* · 随包排空脚本(`scripts/drain-check-local.mjs` 的 `retired-skill-manifest` 判据,经脚本隔壁的编译产物 `dist/http/retired-skill-manifest.js` 取):
|
|
7
|
+
* 升级前停驻、持久化的提交体里仍带它的行,升级后续跑会被引擎按名拒(顶层 `skills[]`)或丢掉那只子代定义(`agents[].skills[]`),运维升级前要能数出来。
|
|
8
|
+
*
|
|
9
|
+
* 「带」= 键在场、值是什么都算(`null` / 空对象同样是「我带了 manifest」)—— 与引擎的退役键表读法同形(`RETIRED_TASK_SPEC_KEYS["skills[].manifest"]`:`at !== undefined`)。
|
|
10
|
+
* 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 本模块**零运行期 import**(随包脚本要在没有服务其余部分的位置加载它,与 `checkpoint-ctx.js` 同一姿势)—— 往里加 import 要先想想脚本那一头。
|
|
13
|
+
*/
|
|
14
|
+
/** 一条技能(提交体 `skills[]` / `agents[].skills[]` 的元素)是否还带退役的 `manifest` 键。 */
|
|
15
|
+
export declare function skillCarriesRetiredManifest(skill: unknown): boolean;
|
|
16
|
+
/** 持久化体里哪一层带着它:顶层 `skills[]`,或某只子代定义的 `agents[].skills[]`。 */
|
|
17
|
+
export type RetiredSkillManifestLayer = "skills" | "agents.skills";
|
|
18
|
+
/**
|
|
19
|
+
* 一份提交体(持久化的 JSON)里有没有技能还带 `manifest`,有则说是哪一层(两层都有 ⇒ 取顶层,先说引擎会响亮拒的那一层);都没有 ⇒ `undefined`。
|
|
20
|
+
* 顶层不是数组 / 子代不是对象 / 子代的 `skills` 不是数组 ⇒ 该处不算(受理门从不放进这种形;盘上若有也不是「带 manifest」)。
|
|
21
|
+
*/
|
|
22
|
+
export declare function retiredSkillManifestLayerOf(body: unknown): RetiredSkillManifestLayer | undefined;
|
|
23
|
+
//# sourceMappingURL=retired-skill-manifest.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function skillCarriesRetiredManifest(skill) {
|
|
2
|
+
return skill !== null && typeof skill === "object" && !Array.isArray(skill) && Object.hasOwn(skill, "manifest");
|
|
3
|
+
}
|
|
4
|
+
const isArray = (v) => Array.isArray(v);
|
|
5
|
+
export function retiredSkillManifestLayerOf(body) {
|
|
6
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
7
|
+
return undefined;
|
|
8
|
+
const { skills, agents } = body;
|
|
9
|
+
if (isArray(skills) && skills.some(skillCarriesRetiredManifest))
|
|
10
|
+
return "skills";
|
|
11
|
+
if (isArray(agents)) {
|
|
12
|
+
for (const agent of agents) {
|
|
13
|
+
if (agent === null || typeof agent !== "object" || Array.isArray(agent))
|
|
14
|
+
continue;
|
|
15
|
+
const own = agent.skills;
|
|
16
|
+
if (isArray(own) && own.some(skillCarriesRetiredManifest))
|
|
17
|
+
return "agents.skills";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=retired-skill-manifest.js.map
|
|
@@ -84,8 +84,9 @@ export interface RouteRow {
|
|
|
84
84
|
* `test/route-shape-roster.test.ts` 层E 的能力位词干也取自这里 —— 域的身份是声明出来的,
|
|
85
85
|
* 不是从函数名反推的。函数真名与它的一致性由 `route-order-freeze` 自检格钉住。 */
|
|
86
86
|
readonly label: string;
|
|
87
|
-
/** **挂载条件**(只有真有条件的行才写):缺席 =
|
|
88
|
-
*
|
|
87
|
+
/** **挂载条件**(只有真有条件的行才写):缺席 = 恒挂载。今天只剩 diagnostics 一行(它没有能力位,缺席形只出现在测试夹具)。
|
|
88
|
+
* 「这台部署上不可用」的面**不**用它:那些面恒挂载、域头按能力事实(`src/capability-facts.ts`)答 501,与能力位同一只函数
|
|
89
|
+
* —— 未挂载落全局 404 会让「这台没有这一面」与「URL 写错了」同码,也违背契约「false ⇒ 501」。 */
|
|
89
90
|
readonly enabled?: (deps: FlatServiceDeps) => boolean;
|
|
90
91
|
}
|
|
91
92
|
export declare const ROUTES: readonly RouteRow[];
|
package/dist/http/route-table.js
CHANGED
|
@@ -52,9 +52,9 @@ export const ROUTES = [
|
|
|
52
52
|
row({ handler: handleRunMemoryCapture, decls: RUN_MEMORY_CAPTURE_ROUTES, position: "post-credential", label: "run-memory-capture" }),
|
|
53
53
|
row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
|
|
54
54
|
row({ handler: handleBackground, decls: BACKGROUND_ROUTES, position: "post-credential", label: "background" }),
|
|
55
|
-
row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader"
|
|
55
|
+
row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader" }),
|
|
56
56
|
row({ handler: handleImages, decls: IMAGES_ROUTES, position: "post-credential", label: "images" }),
|
|
57
|
-
row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant"
|
|
57
|
+
row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant" }),
|
|
58
58
|
row({ handler: handleObservability, decls: OBSERVABILITY_ROUTES, position: "post-credential", label: "observability" }),
|
|
59
59
|
row({ handler: handleDiagnostics, decls: DIAGNOSTICS_ROUTES, position: "post-credential", label: "diagnostics", enabled: (deps) => Boolean(deps.staticWiring) }),
|
|
60
60
|
row({ handler: handleAdoption, decls: ADOPTION_ROUTES, position: "post-credential", label: "adoption" }),
|
|
@@ -62,15 +62,15 @@ export const ROUTES = [
|
|
|
62
62
|
row({ handler: handleAdminDrain, decls: ADMIN_DRAIN_ROUTES, position: "post-credential", label: "admin-drain" }),
|
|
63
63
|
row({ handler: handleDevices, decls: DEVICES_ROUTES, position: "post-credential", label: "devices" }),
|
|
64
64
|
row({ handler: handleAdminConfigRefresh, decls: ADMIN_CONFIG_REFRESH_ROUTES, position: "post-credential", label: "admin-config-refresh" }),
|
|
65
|
-
row({ handler: handleConfigCatalog, decls: CONFIG_CATALOG_ROUTES, position: "post-credential", label: "config-catalog"
|
|
65
|
+
row({ handler: handleConfigCatalog, decls: CONFIG_CATALOG_ROUTES, position: "post-credential", label: "config-catalog" }),
|
|
66
66
|
row({ handler: handleMemoryOptOut, decls: MEMORY_OPTOUT_ROUTES, position: "post-credential", label: "memory-opt-out" }),
|
|
67
67
|
row({ handler: handleMemoryPolicy, decls: MEMORY_POLICY_ROUTES, position: "post-credential", label: "memory-policy" }),
|
|
68
68
|
row({ handler: handleMemoryBundle, decls: MEMORY_BUNDLE_ROUTES, position: "post-credential", label: "memory-bundle" }),
|
|
69
69
|
row({ handler: handleMemoryCompliance, decls: MEMORY_COMPLIANCE_ROUTES, position: "post-credential", label: "memory-compliance" }),
|
|
70
70
|
row({ handler: handleMemoryOrigin, decls: MEMORY_ORIGIN_ROUTES, position: "post-credential", label: "memory-origin" }),
|
|
71
|
-
row({ handler: handleMemoryConsolidation, decls: MEMORY_CONSOLIDATION_ROUTES, position: "post-credential", label: "memory-consolidation"
|
|
71
|
+
row({ handler: handleMemoryConsolidation, decls: MEMORY_CONSOLIDATION_ROUTES, position: "post-credential", label: "memory-consolidation" }),
|
|
72
72
|
row({ handler: handleRules, decls: RULES_ROUTES, position: "post-credential", label: "rules" }),
|
|
73
|
-
row({ handler: handleSharedMemory, decls: SHARED_MEMORY_ROUTES, position: "post-credential", label: "shared-memory"
|
|
73
|
+
row({ handler: handleSharedMemory, decls: SHARED_MEMORY_ROUTES, position: "post-credential", label: "shared-memory" }),
|
|
74
74
|
row({ handler: handleSessionsList, decls: SESSIONS_LIST_ROUTES, position: "post-credential", label: "sessions-list" }),
|
|
75
75
|
row({ handler: handleAgentRoster, decls: AGENT_ROSTER_ROUTES, position: "post-credential", label: "agent-roster" }),
|
|
76
76
|
row({ handler: handleSessions, decls: SESSIONS_ROUTES, position: "post-credential", label: "sessions" }),
|
|
@@ -8,6 +8,7 @@ import { sendJson, sendError } from "../send.js";
|
|
|
8
8
|
import { readinessOf, billableRefusalOf, sendBillableRefusal } from "../readiness.js";
|
|
9
9
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
10
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
|
+
import * as facts from "../../capability-facts.js";
|
|
11
12
|
import { createDurableRun } from "./runs.js";
|
|
12
13
|
export const A2A_PROTOCOL_VERSION = "1.0";
|
|
13
14
|
export const A2A_VERSION_HEADER = "a2a-version";
|
|
@@ -25,13 +26,13 @@ export async function handleA2aServe(req, res, match, ctx) {
|
|
|
25
26
|
const { deps } = ctx;
|
|
26
27
|
switch (match.decl.id) {
|
|
27
28
|
case "a2a-agent-card": {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
29
|
+
const peer = facts.a2aServe(deps);
|
|
30
|
+
if (!peer.available) {
|
|
30
31
|
sendError(res, 404, "not_found.route", "not found");
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
|
|
34
|
-
sendJson(res, 200, buildAgentCard(serve));
|
|
35
|
+
sendJson(res, 200, buildAgentCard(peer.serve));
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
default:
|
|
@@ -44,10 +45,8 @@ const JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
|
44
45
|
const JSONRPC_INVALID_PARAMS = -32602;
|
|
45
46
|
const JSONRPC_INTERNAL_ERROR = -32603;
|
|
46
47
|
const A2A_TASK_NOT_FOUND = -32001;
|
|
47
|
-
const A2A_UNSUPPORTED_OPERATION = -32004;
|
|
48
48
|
const A2A_CONTENT_TYPE_NOT_SUPPORTED = -32005;
|
|
49
49
|
export const A2A_TASK_NOT_FOUND_MESSAGE = "task not found";
|
|
50
|
-
const A2A_NO_RUN_STORE_MESSAGE = "this deployment has no durable run store, so A2A tasks cannot be created or polled (set DB_BACKEND=mysql|pg|local)";
|
|
51
50
|
const A2A_STATE_BY_RUN_STATUS = {
|
|
52
51
|
running: "working",
|
|
53
52
|
completed: "completed",
|
|
@@ -188,11 +187,8 @@ function readBlocking(params) {
|
|
|
188
187
|
function isStaleRun(run, runStaleSec) {
|
|
189
188
|
return run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > runStaleSec * 1000;
|
|
190
189
|
}
|
|
191
|
-
async function waitForRun(ctx, res, taskId, budgetMs) {
|
|
190
|
+
async function waitForRun(ctx, store, res, taskId, budgetMs) {
|
|
192
191
|
const { deps } = ctx;
|
|
193
|
-
const store = deps.runStore;
|
|
194
|
-
if (!store)
|
|
195
|
-
return null;
|
|
196
192
|
const deadline = Date.now() + budgetMs;
|
|
197
193
|
let last = null;
|
|
198
194
|
for (;;) {
|
|
@@ -219,7 +215,7 @@ async function waitForRun(ctx, res, taskId, budgetMs) {
|
|
|
219
215
|
});
|
|
220
216
|
}
|
|
221
217
|
}
|
|
222
|
-
async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
218
|
+
async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
223
219
|
const { deps } = ctx;
|
|
224
220
|
const { rateLimited, quotaExceeded } = ctx.helpers;
|
|
225
221
|
const message = asRecord(params.message, "params.message");
|
|
@@ -232,8 +228,6 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
232
228
|
const skillId = skill?.id;
|
|
233
229
|
const scenario = skill?.scenario;
|
|
234
230
|
const blocking = readBlocking(params);
|
|
235
|
-
if (!deps.runStore)
|
|
236
|
-
throw new RpcError(A2A_UNSUPPORTED_OPERATION, A2A_NO_RUN_STORE_MESSAGE);
|
|
237
231
|
const idemScope = `${ctx.req.source ?? ""}\x1f${gatedPrincipal(req, deps.config) ?? ""}`;
|
|
238
232
|
const fingerprint = createHash("sha256")
|
|
239
233
|
.update(messageId).update("\x1f")
|
|
@@ -248,7 +242,7 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
248
242
|
...(contextId !== undefined ? { sessionId: contextId } : {}),
|
|
249
243
|
...(scenario !== undefined ? { scenario } : {}),
|
|
250
244
|
};
|
|
251
|
-
const replay = await
|
|
245
|
+
const replay = await runStore.getRun(body.taskId).catch(() => undefined);
|
|
252
246
|
if (replay && (replay.owner === null || replay.owner === gatedPrincipal(req, deps.config))) {
|
|
253
247
|
return taskFromRun(replay, isStaleRun(replay, deps.config.runStaleSec));
|
|
254
248
|
}
|
|
@@ -280,7 +274,7 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
280
274
|
}
|
|
281
275
|
const taskId = receiptBody.taskId;
|
|
282
276
|
const sessionId = receiptBody.sessionId;
|
|
283
|
-
const run = await waitForRun(ctx, res, taskId, blocking ? serve.blockingWaitMs : 0);
|
|
277
|
+
const run = await waitForRun(ctx, runStore, res, taskId, blocking ? serve.blockingWaitMs : 0);
|
|
284
278
|
if (run)
|
|
285
279
|
return taskFromRun(run, isStaleRun(run, deps.config.runStaleSec));
|
|
286
280
|
return {
|
|
@@ -298,12 +292,10 @@ function durableIdempotencyTaskId(scope, messageId) {
|
|
|
298
292
|
const hex = b.toString("hex");
|
|
299
293
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
300
294
|
}
|
|
301
|
-
async function handleTasksGet({ req, ctx, params }) {
|
|
295
|
+
async function handleTasksGet({ req, ctx, params, runStore }) {
|
|
302
296
|
const { deps } = ctx;
|
|
303
297
|
const id = asNonEmptyString(params.id, "params.id", MAX_TASK_ID_CHARS);
|
|
304
|
-
|
|
305
|
-
throw new RpcError(A2A_UNSUPPORTED_OPERATION, A2A_NO_RUN_STORE_MESSAGE);
|
|
306
|
-
const run = await deps.runStore.getRun(id);
|
|
298
|
+
const run = await runStore.getRun(id);
|
|
307
299
|
if (!run)
|
|
308
300
|
throw new RpcError(A2A_TASK_NOT_FOUND, A2A_TASK_NOT_FOUND_MESSAGE);
|
|
309
301
|
if (run.owner !== null && gatedPrincipal(req, deps.config) !== run.owner)
|
|
@@ -322,11 +314,12 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
322
314
|
default:
|
|
323
315
|
return assertNever(match.decl.id);
|
|
324
316
|
}
|
|
325
|
-
const
|
|
326
|
-
if (!
|
|
317
|
+
const peer = facts.a2aServe(deps);
|
|
318
|
+
if (!peer.available) {
|
|
327
319
|
sendError(res, 404, "not_found.route", "not found");
|
|
328
320
|
return;
|
|
329
321
|
}
|
|
322
|
+
const { serve, runStore } = peer;
|
|
330
323
|
const requestedVersion = req.headers[A2A_VERSION_HEADER];
|
|
331
324
|
res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
|
|
332
325
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
@@ -372,7 +365,7 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
372
365
|
}
|
|
373
366
|
deps.logger?.debug?.("a2a_serve_rpc", { method, ...(typeof requestedVersion === "string" ? { requestedVersion: requestedVersion.slice(0, 32) } : {}) });
|
|
374
367
|
try {
|
|
375
|
-
const result = await handler({ req, res, ctx, params: params, serve });
|
|
368
|
+
const result = await handler({ req, res, ctx, params: params, serve, runStore });
|
|
376
369
|
if (result === undefined) {
|
|
377
370
|
deps.metrics?.inc("a2a_serve_rpc_total", { method, outcome: "refused" });
|
|
378
371
|
return;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
2
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
+
import * as facts from "../../capability-facts.js";
|
|
3
4
|
import { assertNever } from "../route-ctx.js";
|
|
4
5
|
export const ADMIN_CONFIG_REFRESH_PATH = "/v1/admin/config/refresh";
|
|
5
|
-
const OPERATOR_ONLY_MESSAGE = "config refresh is operator-only: the caller principal is not in OPERATOR_PRINCIPALS
|
|
6
|
+
const OPERATOR_ONLY_MESSAGE = "config refresh is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
|
|
6
7
|
export async function handleAdminConfigRefresh(req, res, match, ctx) {
|
|
7
8
|
const { deps } = ctx;
|
|
8
9
|
switch (match.decl.id) {
|
|
@@ -12,6 +13,10 @@ export async function handleAdminConfigRefresh(req, res, match, ctx) {
|
|
|
12
13
|
sendPrincipalRefusal(res, req, deps.config);
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
16
|
+
if (!facts.operatorLane(deps).available) {
|
|
17
|
+
sendOperatorPrincipalsRequired(res);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
15
20
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
16
21
|
sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
|
|
17
22
|
return;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
2
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
+
import * as facts from "../../capability-facts.js";
|
|
3
4
|
import { assertNever } from "../route-ctx.js";
|
|
4
5
|
export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
|
|
5
6
|
export const MAX_DRAIN_REASON_CHARS = 256;
|
|
6
|
-
const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS
|
|
7
|
+
const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
|
|
7
8
|
export async function handleAdminDrain(req, res, match, ctx) {
|
|
8
9
|
const { deps } = ctx;
|
|
9
10
|
switch (match.decl.id) {
|
|
@@ -13,6 +14,10 @@ export async function handleAdminDrain(req, res, match, ctx) {
|
|
|
13
14
|
sendPrincipalRefusal(res, req, deps.config);
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
17
|
+
if (!facts.operatorLane(deps).available) {
|
|
18
|
+
sendOperatorPrincipalsRequired(res);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
16
21
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
17
22
|
sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
|
|
18
23
|
return;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
|
-
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
+
import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
4
4
|
import { assertNever } from "../route-ctx.js";
|
|
5
|
+
import * as facts from "../../capability-facts.js";
|
|
5
6
|
import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
|
|
6
7
|
export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
|
|
7
8
|
const PutBodySchema = z.object({ allow: z.boolean(), note: z.string().min(1).max(500).optional() }).strict();
|
|
@@ -15,16 +16,29 @@ export async function handleMemoryOptOut(req, res, match, ctx) {
|
|
|
15
16
|
sendPrincipalRefusal(res, req, deps.config);
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
19
|
+
const fact = facts.memoryOptOutGrant(deps);
|
|
20
|
+
if (!fact.available && fact.why === "operator_principals_empty") {
|
|
21
|
+
sendOperatorPrincipalsRequired(res);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
18
24
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
19
|
-
sendError(res, 403, "auth.operator_only", "memory opt-out grant administration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS
|
|
25
|
+
sendError(res, 403, "auth.operator_only", "memory opt-out grant administration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS");
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
28
|
const operator = principal;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
if (!fact.available) {
|
|
30
|
+
switch (fact.why) {
|
|
31
|
+
case "memory_optout_grant_absent":
|
|
32
|
+
sendError(res, 501, "capability.memory_optout_grant_required", "this deployment has no memory opt-out grant table: the per-principal memory-capture opt-out authorization store lives on a SQL store backend (DB_BACKEND=mysql|pg) and the local backend deliberately keeps no companion store for forgetting — wire a SQL backend to administer opt-out grants");
|
|
33
|
+
return;
|
|
34
|
+
case "operator_principals_empty":
|
|
35
|
+
sendOperatorPrincipalsRequired(res);
|
|
36
|
+
return;
|
|
37
|
+
default:
|
|
38
|
+
return assertNever(fact.why);
|
|
39
|
+
}
|
|
27
40
|
}
|
|
41
|
+
const store = fact.store;
|
|
28
42
|
if (target !== undefined && (target === null || memoryOptOutPrincipalShapeProblem(target) !== undefined)) {
|
|
29
43
|
sendError(res, 400, "request.path_malformed", "invalid memory opt-out grant principal — the path segment must percent-decode to a non-empty principal of at most 190 chars with no leading/trailing whitespace or control characters (the deployment default row is addressed by PUT on the collection root, not by a path segment)");
|
|
30
44
|
return;
|