@sema-agent/server 7.9.0 → 7.11.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.
- package/USAGE.md +6 -0
- package/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/approval-reconciler.d.ts +1 -1
- package/dist/approval-reconciler.js +1 -1
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +38 -5
- package/dist/boot/parked-revive-gate.js +53 -6
- package/dist/boot/reapers.js +3 -3
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/capabilities/repo-tools.d.ts +36 -2
- package/dist/capabilities/repo-tools.js +125 -11
- package/dist/capabilities/scenarios.d.ts +2 -2
- package/dist/capabilities/scenarios.js +1 -1
- package/dist/config-types.d.ts +22 -2
- package/dist/config.js +44 -2
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.js +1 -1
- package/dist/git-api-kind.d.ts +7 -0
- package/dist/git-api-kind.js +8 -0
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/capabilities.js +12 -0
- package/dist/http/routes/rules.d.ts +23 -0
- package/dist/http/routes/rules.js +117 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +19 -1
- package/dist/http/server.js +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +54 -5
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/plugins/adoption-log-sql.d.ts +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +22 -9
- package/dist/plugins/checkpoint-store-sql.js +40 -29
- package/dist/plugins/local-checkpoint-store.d.ts +11 -1
- package/dist/plugins/local-checkpoint-store.js +10 -2
- package/dist/plugins/memory-engine-pg.js +3 -3
- package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
- package/dist/plugins/permission-rule-store-sql.js +817 -0
- package/dist/plugins/pg-pool.js +44 -7
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +30 -0
- package/dist/plugins/store-backend.js +14 -0
- package/dist/plugins/tidb-pool.js +55 -8
- package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
- package/dist/plugins/workflow-journal-store-sql.js +6 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +12 -12
- package/dist/rules-consent.d.ts +126 -0
- package/dist/rules-consent.js +198 -0
- package/dist/run-local.js +2 -2
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/tool-approval.d.ts +55 -0
- package/dist/tool-approval.js +124 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/package.json +3 -3
package/dist/config-types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ElicitationThrottle } from "./elicitation.js";
|
|
|
12
12
|
import type { QuestionThrottle } from "./question.js";
|
|
13
13
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
14
14
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
15
|
+
import type { GitApiKind } from "./git-api-kind.js";
|
|
15
16
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
16
17
|
* scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
|
|
17
18
|
export interface ScopedMcpServer {
|
|
@@ -866,6 +867,21 @@ export interface ServiceConfigFlat {
|
|
|
866
867
|
* cap → posture-gated like askQuestion (single-user turnkey → ON; multi-tenant opt-in);
|
|
867
868
|
* `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([819]⑤ fail-closed). */
|
|
868
869
|
toolApprovalEnabled: boolean;
|
|
870
|
+
/**
|
|
871
|
+
* #154 车二(core design/179 持久化权限规则「不再询问」车道)**总开关**。`PERMISSION_RULES_ENABLED`,
|
|
872
|
+
* **默认 OFF**。
|
|
873
|
+
*
|
|
874
|
+
* 🔴 为什么是显式 opt-in 而不是「有 SQL 后端就自动上」(codex 交叉复审 round7 [high] 一,验真后收):
|
|
875
|
+
* 这条车道**只会加放行、不会减**,而 v1 **还没有撤销面**(列出 / 删除自己已存规则的口子没建;core 的
|
|
876
|
+
* `removePersistedRule` 在包里,但本仓零调用点)。一次误导入的全局规则因此会一直生效,租户自己拿它
|
|
877
|
+
* 没办法。在撤销面落地之前,**默认不开**是这条轴唯一诚实的姿势;开它的部署等于明说「我接受这条状态,
|
|
878
|
+
* 并且知道现在只能靠运维改库来收回」。
|
|
879
|
+
*
|
|
880
|
+
* 关 ⇒ 规则店根本不装配:`RunnerDeps.permissionRuleStore` 缺席(引擎的 `permissionRules.storeWired`
|
|
881
|
+
* 如实报 false)、ask 帧不带 `ruleSuggestions`、回决的 `persistRule` 拒、`POST /v1/rules/cc-import/*`
|
|
882
|
+
* 两口 501。三张表仍随中央 `ensureSchema` 建(纯 CREATE、零行),这不是行为面。
|
|
883
|
+
*/
|
|
884
|
+
permissionRulesEnabled: boolean;
|
|
869
885
|
/** #151 车2(design/172 §3.3 D3,窗长三元的安全余量):`ToolApprovalCoordinator` 的可选 `windowMarginMs`
|
|
870
886
|
* 构造项——有效窗 = `min(ttlMs, legRemainingMs − 本值)`,余量不足 ⇒ 不开窗直接走窗到期同路(park)。
|
|
871
887
|
* 仅在装配点把 `ToolApprovalRunContext.legDeadlineMonotonic` 传给协调器(车3 的活)时才实际生效——本
|
|
@@ -966,6 +982,10 @@ export interface ServiceConfigFlat {
|
|
|
966
982
|
gitApiBaseUrl?: string;
|
|
967
983
|
/** Read-only Git host token (server-side only; never reaches the model). */
|
|
968
984
|
gitApiToken?: string;
|
|
985
|
+
/** Git host API dialect for the repo tools (env GIT_API_KIND, closed set gitea|github; default gitea).
|
|
986
|
+
* "github" = api.github.com or GHE `/api/v3` root — the Gitea client's `/api/v1` + `.diff` shapes
|
|
987
|
+
* 404 there (P1-2: our own repos live on GitHub and were unreviewable). Unknown word = refuse boot. */
|
|
988
|
+
gitApiKind: GitApiKind;
|
|
969
989
|
/** S3-TOB 设计 §1.3:memory 持久面方言(env MEMORY_ENGINE_BACKEND,缺省 file)。boot-only。 */
|
|
970
990
|
memoryEngineBackend: "file" | "pg" | "tidb";
|
|
971
991
|
/** CC `workflowSizeGuideline` (advisory, injected into the Workflow tool card via
|
|
@@ -1004,7 +1024,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
|
|
|
1004
1024
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
|
|
1005
1025
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
1006
1026
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
1007
|
-
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
1027
|
+
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
1008
1028
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
1009
1029
|
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
1010
1030
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
|
@@ -1017,7 +1037,7 @@ export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachme
|
|
|
1017
1037
|
/** 组:observability(可观测)。 */
|
|
1018
1038
|
export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
|
|
1019
1039
|
/** 组:integrations(外部集成)。`mcpServers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
|
|
1020
|
-
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "defaultScenario" | "skillsDir" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
1040
|
+
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
1021
1041
|
/** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
|
|
1022
1042
|
* `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
|
|
1023
1043
|
export interface ServiceConfigGroups {
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js"; // d
|
|
|
7
7
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
8
8
|
import { isV2ScopeKey } from "./memory-scope.js"; // A-002.4: v2 scope 前缀词表的单一属主(纯谓词,无反向依赖)
|
|
9
9
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
10
|
+
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js"; // P1-2: 方言词表叶模块(config 不得值引 capabilities 层)
|
|
10
11
|
function csv(name) {
|
|
11
12
|
return (process.env[name] ?? "")
|
|
12
13
|
.split(",")
|
|
@@ -1052,6 +1053,10 @@ function parseApprovalDomain(ctx) {
|
|
|
1052
1053
|
ttlMs: numEnvBounded("QUESTION_TTL_MS", String(DEFAULT_QUESTION_THROTTLE.ttlMs), 1_000, 3_600_000),
|
|
1053
1054
|
},
|
|
1054
1055
|
toolApprovalEnabled: postureOn("TOOL_APPROVAL_ENABLED"), // [816]/[820]② live tool-approval HITL; posture-gated (single-user → ON), mirrors askQuestion
|
|
1056
|
+
// #154 车二:持久化权限规则车道的总开关。**默认 OFF**,理由(撤销面未落地之前只加放行不减)逐字见
|
|
1057
|
+
// `ServiceApprovalConfig.permissionRulesEnabled`。**不**做 posture 派生:这条轴与「单用户交钥匙」
|
|
1058
|
+
// 无关,它取决于运维接不接受「暂时只能改库收回」,那必须是一次显式表态。
|
|
1059
|
+
permissionRulesEnabled: boolEnv("PERMISSION_RULES_ENABLED", false),
|
|
1055
1060
|
// #151 车2(design/172 §3.3 D3):ToolApprovalCoordinator 窗长三元的安全余量。numEnv fail-loud 形,
|
|
1056
1061
|
// 照邻居旋钮(approvalTimeoutSec 等)抄——坏形(非数字)在启动期炸,不静默折成 NaN。
|
|
1057
1062
|
streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
|
|
@@ -1501,6 +1506,42 @@ function parseObservabilityDomain() {
|
|
|
1501
1506
|
: undefined,
|
|
1502
1507
|
};
|
|
1503
1508
|
}
|
|
1509
|
+
/** GIT_API_KIND:repo 工具的 Git-host 方言(闭集 gitea|github,缺省 gitea)。
|
|
1510
|
+
* 未知词=拒启(安全轴词表纪律);另一坏形也拒启:kind 缺省/gitea 而 BASEURL 指向 github.com——
|
|
1511
|
+
* Gitea 形状(`/api/v1` 前缀、`.diff` 后缀)对 GitHub 恒 404,与其在任务运行时逐调用 loud 404,
|
|
1512
|
+
* 不如启动即指路(#123 FLEET_ADVERTISE_ADDRESS 坏形响亮拒同族)。 */
|
|
1513
|
+
function parseGitApiKind(raw, baseUrl) {
|
|
1514
|
+
const kind = raw ?? "gitea";
|
|
1515
|
+
if (!isGitApiKind(kind)) {
|
|
1516
|
+
throw new Error(`GIT_API_KIND="${raw}" is not a known Git host kind (expected one of: ${GIT_API_KINDS.join(", ")})`);
|
|
1517
|
+
}
|
|
1518
|
+
if (!baseUrl)
|
|
1519
|
+
return kind;
|
|
1520
|
+
// 配了就必须是合法 URL(USAGE 本就要求带 scheme 的完整 baseURL;坏 URL 此前静默过门、逐调用 fetch
|
|
1521
|
+
// 才炸 —— 启动即拒;codex F3)。尾点主机(api.github.com.)DNS 等价,归一后再比对,防绕过。
|
|
1522
|
+
if (!URL.canParse(baseUrl)) {
|
|
1523
|
+
throw new Error(`GIT_API_BASEURL="${baseUrl}" is not a valid URL (expected e.g. https://git.example.com)`);
|
|
1524
|
+
}
|
|
1525
|
+
const u = new URL(baseUrl);
|
|
1526
|
+
const host = u.hostname.replace(/\.$/, "").toLowerCase();
|
|
1527
|
+
const githubFamily = host === "github.com" || host === "api.github.com" || host.endsWith(".github.com");
|
|
1528
|
+
if (kind === "gitea" && githubFamily) {
|
|
1529
|
+
throw new Error(`GIT_API_BASEURL points at ${host} but GIT_API_KIND is "gitea" — the Gitea API shapes 404 on GitHub. Set GIT_API_KIND=github (base url should be https://api.github.com, or your GHE /api/v3 root).`);
|
|
1530
|
+
}
|
|
1531
|
+
if (kind === "github") {
|
|
1532
|
+
// 对称校验(codex F3):web 域名/带 path 的 api 根都会让每个仓操作 404,而客户端照建、场景照宣可用。
|
|
1533
|
+
if (githubFamily && host !== "api.github.com") {
|
|
1534
|
+
throw new Error(`GIT_API_BASEURL host ${host} is the GitHub WEB host — the API root is https://api.github.com (a repo page URL is not an API base).`);
|
|
1535
|
+
}
|
|
1536
|
+
if (host === "api.github.com" && u.pathname !== "/" && u.pathname !== "") {
|
|
1537
|
+
throw new Error(`GIT_API_BASEURL for github.com must be exactly https://api.github.com (no path; got path "${u.pathname}"). GHE uses https://<ghe-host>/api/v3.`);
|
|
1538
|
+
}
|
|
1539
|
+
if (u.search || u.hash) {
|
|
1540
|
+
throw new Error(`GIT_API_BASEURL must not carry a query or fragment (got "${baseUrl}")`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return kind;
|
|
1544
|
+
}
|
|
1504
1545
|
/** 域:misc-integration(外部集成)—— plugins clone 白名单、git/OA 面、场景与 skill 目录、sema-registry 接入。 */
|
|
1505
1546
|
function parseIntegrationsDomain() {
|
|
1506
1547
|
return {
|
|
@@ -1509,6 +1550,7 @@ function parseIntegrationsDomain() {
|
|
|
1509
1550
|
configBootFetchBudgetMs: Math.max(0, numEnv("CONFIG_BOOT_FETCH_BUDGET_MS", "1500")), // boot 首拉抢答窗;过窗转后台补齐(clay 2026-07-17:本地 5s 启动=黑洞中心同步等待)
|
|
1510
1551
|
gitApiBaseUrl: process.env.GIT_API_BASEURL,
|
|
1511
1552
|
gitApiToken: process.env.GIT_API_TOKEN,
|
|
1553
|
+
gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
|
|
1512
1554
|
// BEHAVIOR CHANGE([891] clay 硬裁定):出厂缺省场景 default→code(CC 编码 persona 蒸馏版)——
|
|
1513
1555
|
// 编码产品出厂态对标 CC 出厂态,通用域中性 persona 用 DEFAULT_SCENARIO=default 显式选回。
|
|
1514
1556
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
@@ -1540,7 +1582,7 @@ const MODEL_PLANE_GROUP_KEYS = [
|
|
|
1540
1582
|
const APPROVAL_GROUP_KEYS = [
|
|
1541
1583
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
1542
1584
|
"approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
1543
|
-
"resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
|
|
1585
|
+
"resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
|
|
1544
1586
|
"sensitiveWritePatterns", "manualModeShellGate",
|
|
1545
1587
|
];
|
|
1546
1588
|
const MEMORY_GROUP_KEYS = [
|
|
@@ -1570,7 +1612,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
|
|
|
1570
1612
|
];
|
|
1571
1613
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
1572
1614
|
const INTEGRATIONS_GROUP_KEYS = [
|
|
1573
|
-
"pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "defaultScenario",
|
|
1615
|
+
"pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
1574
1616
|
"skillsDir", "configCenter", "configProvider", "configLocalDir",
|
|
1575
1617
|
];
|
|
1576
1618
|
/** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
|
|
@@ -43,7 +43,7 @@ export declare const FLEET_SNAPSHOT_TERMINAL_MAX_ROWS = 20;
|
|
|
43
43
|
* 每个终态**扫描面**的行数(店侧下推的 limit)。它比 {@link FLEET_SNAPSHOT_TERMINAL_MAX_ROWS} 大是有
|
|
44
44
|
* 具体病灶的(codex R1-M3,红先复现):店侧分页按 `createdAt DESC` 排(core 契约 + 两个 SQL 实现皆然),
|
|
45
45
|
* 而本窗的判据是**结束时刻**——一条跑了三天、刚刚才结束的 workflow(正是用户此刻要看的那条)会被 20 条
|
|
46
|
-
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `
|
|
46
|
+
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `ended_at_ms DESC`
|
|
47
47
|
* 的窄查询(要过真双库门,已作移交项记在发车说明里)。
|
|
48
48
|
*
|
|
49
49
|
* 残余(如实记档):同 scope 同一终态下,若有超过本值条**更晚创建**的行,那条"早创建、刚结束"的行仍会
|
|
@@ -44,7 +44,7 @@ export const FLEET_SNAPSHOT_TERMINAL_MAX_ROWS = 20;
|
|
|
44
44
|
* 每个终态**扫描面**的行数(店侧下推的 limit)。它比 {@link FLEET_SNAPSHOT_TERMINAL_MAX_ROWS} 大是有
|
|
45
45
|
* 具体病灶的(codex R1-M3,红先复现):店侧分页按 `createdAt DESC` 排(core 契约 + 两个 SQL 实现皆然),
|
|
46
46
|
* 而本窗的判据是**结束时刻**——一条跑了三天、刚刚才结束的 workflow(正是用户此刻要看的那条)会被 20 条
|
|
47
|
-
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `
|
|
47
|
+
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `ended_at_ms DESC`
|
|
48
48
|
* 的窄查询(要过真双库门,已作移交项记在发车说明里)。
|
|
49
49
|
*
|
|
50
50
|
* 残余(如实记档):同 scope 同一终态下,若有超过本值条**更晚创建**的行,那条"早创建、刚结束"的行仍会
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Git-host API dialect word table (P1-2). Leaf module — the single owner shared by the config layer
|
|
2
|
+
* (env parse/validation) and `capabilities/repo-tools` (the two client implementations); config must not
|
|
3
|
+
* value-import the capabilities layer (module-cycle gate §G), so the table lives below both. */
|
|
4
|
+
export declare const GIT_API_KINDS: readonly ["gitea", "github"];
|
|
5
|
+
export type GitApiKind = (typeof GIT_API_KINDS)[number];
|
|
6
|
+
export declare function isGitApiKind(v: unknown): v is GitApiKind;
|
|
7
|
+
//# sourceMappingURL=git-api-kind.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Git-host API dialect word table (P1-2). Leaf module — the single owner shared by the config layer
|
|
2
|
+
* (env parse/validation) and `capabilities/repo-tools` (the two client implementations); config must not
|
|
3
|
+
* value-import the capabilities layer (module-cycle gate §G), so the table lives below both. */
|
|
4
|
+
export const GIT_API_KINDS = ["gitea", "github"];
|
|
5
|
+
export function isGitApiKind(v) {
|
|
6
|
+
return typeof v === "string" && GIT_API_KINDS.includes(v);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=git-api-kind.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/183 §4.3 —— **收编面**(operator lane):`POST /v1/adoption` 发起、`GET /v1/adoption/:id` 读状态。
|
|
3
|
+
*
|
|
4
|
+
* 形态:一次性的**部署级动作**,不是常驻业务面 —— 所以它 billable=false(零模型工作)、operator-only、
|
|
5
|
+
* 且在没有 SQL 后端的部署上诚实 501(收编重绑的是多租身份轴,local 后端根本没有那个轴)。
|
|
6
|
+
*
|
|
7
|
+
* ── 三条拒的分家(183 §7.3 拒绝可判别)──────────────────────────────────────────────────────────
|
|
8
|
+
* · `adoption.source_already_bound` (409):同一个源已绑到**别的**目的地 = 二次收编/多租转让,
|
|
9
|
+
* 183 D7 明确拒,非本协议射程。与「已收编回执」不同形 —— 后者是 200。
|
|
10
|
+
* · `adoption.destination_conflict` (409):目的地侧已有与源侧重叠的逻辑键。**响应列出冲突表族**,
|
|
11
|
+
* 运维据此知道该去清理哪张表;源/目的地两侧字节零变更。
|
|
12
|
+
* · `adoption.destination_unrepresentable` (409):目的地身份装不进这次收编会**派生**出来的某个键
|
|
13
|
+
* (典型:memory 的 `proj:` 键 = 新 tenant 段 + 原有项目段后缀,合起来越过列宽)。与上一条分家的理由:
|
|
14
|
+
* 运维的动作完全不同 —— 那条要去清理另一个 principal 的行,这条要换一个更短的目的地身份。
|
|
15
|
+
* · `not_found.adoption` (404):operator 拿着一个不存在的 id 来读。
|
|
16
|
+
*
|
|
17
|
+
* 幂等:同参数重跑秒回**同一份** `AdoptionReceipt`(`immutableReport` 逐字节恒同 —— 它在 phase 6
|
|
18
|
+
* 一次落库,此后原样回放)。并发同参 POST 由 `adoption_log.from_principal` 的 UNIQUE 仲裁:恰一行落库,
|
|
19
|
+
* 两条连接拿到同一个 adoptionId、同一份回执。
|
|
20
|
+
*
|
|
21
|
+
* 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
|
|
22
|
+
*/
|
|
23
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
24
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
25
|
+
export declare function handleAdoption(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=adoption.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createAdoptionRunner } from "../../adoption/runner.js";
|
|
2
|
+
import { AdoptionRequestSchema } from "../../adoption/wire.js";
|
|
3
|
+
import { checkAdoptionWidths } from "../../adoption/plan.js";
|
|
4
|
+
import { sendJson, sendError } from "../send.js";
|
|
5
|
+
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
6
|
+
const ADOPTION_ID_RE = /^\/v1\/adoption\/([^/]+)$/;
|
|
7
|
+
/** 结局 → wire。**拒从不走 200 回执包络**(见顶注三条拒的分家)。
|
|
8
|
+
* 成功码写死 200(不收 `okStatus` 参数):一个**表达式**状态码会让 errorCode 归一门读不出这是成功面,
|
|
9
|
+
* 而且本面本来就只有一个成功码 —— 同参重跑必须与首次同形,重跑创建不了任何东西。 */
|
|
10
|
+
function sendOutcome(res, out) {
|
|
11
|
+
switch (out.kind) {
|
|
12
|
+
case "receipt":
|
|
13
|
+
sendJson(res, 200, { adoptionId: out.adoptionId, ...out.receipt });
|
|
14
|
+
return;
|
|
15
|
+
case "source_already_bound":
|
|
16
|
+
sendError(res, 409, "adoption.source_already_bound", "this source principal is already bound to a different adoption target", {
|
|
17
|
+
adoptionId: out.adoptionId,
|
|
18
|
+
fromPrincipal: out.fromPrincipal,
|
|
19
|
+
boundTo: out.boundTo,
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
// 闭集穷举:`AdoptionRejectCode` 加一个成员而这里没加臂 ⇒ **编译红**(拒绝码是 wire 契约,
|
|
23
|
+
// 不许有一个走到默认臂的静默码)。
|
|
24
|
+
case "rejected":
|
|
25
|
+
switch (out.code) {
|
|
26
|
+
case "adoption.destination_conflict":
|
|
27
|
+
sendError(res, 409, "adoption.destination_conflict", "the destination principal already holds rows sharing a logical key with the source", {
|
|
28
|
+
adoptionId: out.adoptionId,
|
|
29
|
+
detail: out.detail,
|
|
30
|
+
conflicts: out.conflicts,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
case "adoption.destination_unrepresentable":
|
|
34
|
+
sendError(res, 409, "adoption.destination_unrepresentable", "the destination principal does not fit every key this adoption would derive from it", {
|
|
35
|
+
adoptionId: out.adoptionId,
|
|
36
|
+
detail: out.detail,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function handleAdoption(req, res, url, ctx) {
|
|
44
|
+
const miss = { fell: false };
|
|
45
|
+
await handleAdoptionBody(req, res, url, ctx, miss);
|
|
46
|
+
return !miss.fell;
|
|
47
|
+
}
|
|
48
|
+
async function handleAdoptionBody(req, res, url, ctx, miss) {
|
|
49
|
+
const { deps } = ctx;
|
|
50
|
+
const { readJson } = ctx.helpers;
|
|
51
|
+
const isPost = req.method === "POST" && url === "/v1/adoption";
|
|
52
|
+
const idMatch = req.method === "GET" ? ADOPTION_ID_RE.exec(url) : null;
|
|
53
|
+
if (!isPost && !idMatch) {
|
|
54
|
+
miss.fell = true;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// 门序 = operator 面的既有口径:身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。
|
|
58
|
+
// 授权在能力之前:一个够不着任何东西的调用方不该从「有没有 SQL 后端」上读出部署形态。
|
|
59
|
+
const principal = gatedPrincipal(req, deps.config); // direct-door safe: verified identity, never the spoofable header
|
|
60
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
61
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
65
|
+
sendError(res, 403, "auth.operator_only", "adoption is an operator-only deployment action");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const store = deps.backend?.adoptionLog?.();
|
|
69
|
+
if (!store) {
|
|
70
|
+
sendError(res, 501, "capability.adoption_store_required", "adoption requires a SQL store backend (DB_BACKEND=mysql|pg)");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const runner = createAdoptionRunner({ store, dialect: deps.backend?.kind === "pg" ? "pg" : "tidb", now: Date.now, logger: deps.logger });
|
|
74
|
+
if (idMatch) {
|
|
75
|
+
const id = ctx.helpers.safeDecode(idMatch[1]);
|
|
76
|
+
if (id === null) {
|
|
77
|
+
// 复用既有的 `request.id_invalid`(sessions/attachments 同族),不为一条新路由铸第二个同义码
|
|
78
|
+
// ——附录 A 的判据②:只有消费端真会分支时才铸新码。
|
|
79
|
+
sendError(res, 400, "request.id_invalid", "invalid adoption id");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const out = await runner.get(id);
|
|
83
|
+
if (!out) {
|
|
84
|
+
sendError(res, 404, "not_found.adoption", "adoption not found");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
sendOutcome(res, out);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = await readJson(req);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const parsed = AdoptionRequestSchema.safeParse(raw);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
sendError(res, 400, "request.body_shape", "invalid adoption body — expected {fromPrincipal, toPrincipal} as non-empty strings");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { fromPrincipal, toPrincipal } = parsed.data;
|
|
104
|
+
// 🔴 列宽校验在**铸行之前**(codex R1-F3):合法长度的 principal 也可能装不进最窄的那根被写列,
|
|
105
|
+
// 而那种失败若发生在腿的中途,phase 会永远停在 INTENT —— 每次重发和每次 boot 扫描都再撞一次同样的墙。
|
|
106
|
+
const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
|
|
107
|
+
if (tooWide !== undefined) {
|
|
108
|
+
sendError(res, 400, "request.field_invalid", "the adoption principals do not fit every identity column and derived key this adoption must write", { detail: tooWide });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (fromPrincipal === toPrincipal) {
|
|
112
|
+
// 183 §8 的「nothing to adopt」分支:同名不是一次无操作的成功,它是一个提错了的请求
|
|
113
|
+
// ——铸一行 adoption_log 会永久占掉那个 from 的 UNIQUE 名额(D7 之后再也收编不了),所以拒在入库之前。
|
|
114
|
+
sendError(res, 400, "request.field_invalid", "fromPrincipal and toPrincipal are the same — nothing to adopt");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const out = await runner.start(fromPrincipal, toPrincipal);
|
|
118
|
+
sendOutcome(res, out);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=adoption.js.map
|
|
@@ -178,9 +178,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
178
178
|
};
|
|
179
179
|
}))
|
|
180
180
|
// severity DESC, then **explicitly** oldest-first within a tier ([2027] 第五节欠单②,红先绿后:
|
|
181
|
-
// test/assistant-triage-ordering.test.ts)。旧姿势只比 severity,靠「listByScope 是
|
|
181
|
+
// test/assistant-triage-ordering.test.ts)。旧姿势只比 severity,靠「listByScope 是 created_at_ms ASC
|
|
182
182
|
// + Array#sort 稳定」来兑现 §2 那句 oldest-first —— 而那个前提**只在本仓的 SQL 后端成立**
|
|
183
|
-
// (checkpoint-store-sql.ts 的 `ORDER BY
|
|
183
|
+
// (checkpoint-store-sql.ts 的 `ORDER BY created_at_ms ASC`):core 的 `CheckpointStore.listByScope` 接口
|
|
184
184
|
// 零顺序声明,InMemory/File 两个实现直接遍历 Map(LOCAL 车道包的正是 File store)⇒ 那句承诺过去是
|
|
185
185
|
// **后端相关**的。比较器自带兜底后它与 store 顺序无关。形与 listPending 的同族 sort 逐字一致
|
|
186
186
|
// (checkpoint-store-sql.ts `|| a.createdAt - b.createdAt`)。createdAt 缺席(pre-1.116 投影的老行)
|
|
@@ -81,6 +81,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
81
81
|
// the old caps honestly hides the affordances (the adaptation was pre-announced).
|
|
82
82
|
memory: false,
|
|
83
83
|
memoryWrite: false,
|
|
84
|
+
// design/177 —— org 共享记忆库只读面。判据**逐字**等于 server.ts 里那个域的挂载合取式(供给面 ∧
|
|
85
|
+
// org 折叠面),所以"说 yes ⟺ 面真能用"是结构性的:少一个,域不挂载、这里也翻假,消费方看到的
|
|
86
|
+
// 与它真能打通的永远一致。折叠面缺席时刻意不报 true —— 一个没有成员性判据的共享读面比没有更糟。
|
|
87
|
+
sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
|
|
88
|
+
// design/183 form b:收编面(operator lane)。谓词与 handleAdoption 的 501 同源:SQL backend 的
|
|
89
|
+
// adoptionLog 工厂在场(local backend 刻意不实现——单用户数据根没有多租身份轴可重绑)。
|
|
90
|
+
// SDK 半场车逮的能力位缺口:两条新车道此前只能 trial-by-501,违本仓「says yes ⟺ route works」姿势。
|
|
91
|
+
adoption: Boolean(deps.backend?.adoptionLog),
|
|
92
|
+
// #154 车二:规则车道(cc-import 两口 + respond persistRule 兑付口)。谓词与 /v1/rules/cc-import
|
|
93
|
+
// 的 501 同源:ruleConsent lane 在场 ⟺ PERMISSION_RULES_ENABLED=true ∧ 规则店 wired(默认 OFF ⇒
|
|
94
|
+
// 恒 false=诚实;撤销面落地前多租户部署不该看到 true)。
|
|
95
|
+
permissionRules: Boolean(deps.ruleConsent),
|
|
84
96
|
// CLI surfaces: /v1/policy is always served (config surface); /v1/usage returns real
|
|
85
97
|
// cumulative spend only when a per-principal quota is configured (else enabled:false).
|
|
86
98
|
policy: true,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #154 车二 —— **CC settings 导入两口**(design/179 §7 的 wire 半场)。
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/rules/cc-import/prepare → 预览 + 一张**一次性、principal 绑定、有期、载荷绑定**的票
|
|
5
|
+
* POST /v1/rules/cc-import/redeem → 原子消费票 → confirm → 批量兑付进店
|
|
6
|
+
*
|
|
7
|
+
* **lane = principal**(不是 operator):用户导的是**他自己**的规则,一条规则的语义就是「这个人自己
|
|
8
|
+
* 说过 yes」。operator 在这条路上没有位置——他既不该替租户扩权,也不该被要求代人点确认。
|
|
9
|
+
* 因此本域**只认已验明的 principal**,连 `explicitOperatorOk` 的旁路都不给:一个 operator 想给自己
|
|
10
|
+
* 导规则,用他自己的 principal 走同一条路即可(那时他就是租户)。
|
|
11
|
+
*
|
|
12
|
+
* **billable = false**:两口都不烧模型(读 settings 文本、写规则行),不进 `isBillableSubmitPath`。
|
|
13
|
+
*
|
|
14
|
+
* 🔴 **四类拒绝在 wire 上同形 404**(信任边界 ①):unknown ticket / 别人的 ticket / 过期 / 已用过 ——
|
|
15
|
+
* 四个成因返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它在别处存在」)。分类只进服务端
|
|
16
|
+
* 日志,不上 wire。同族先例 = run/trace 属主门与 `POST /v1/tasks/:id/asks/:askId/decision` 的 404 姿势。
|
|
17
|
+
*/
|
|
18
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
19
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
20
|
+
export declare const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
21
|
+
export declare const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
22
|
+
export declare function handleRules(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
23
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sendJson, sendError } from "../send.js";
|
|
3
|
+
import { gatedPrincipal } from "../principal-gate.js";
|
|
4
|
+
import { createLogger } from "../../observability/logger.js";
|
|
5
|
+
// 等待秒数的单一真源在车道:它同时被「放回认领时判这张票还撑不撑得过这段窗」用到(见那侧头注)。
|
|
6
|
+
import { RULE_IMPORT_RETRY_AFTER_SEC } from "../../rules-consent.js";
|
|
7
|
+
const logger = createLogger();
|
|
8
|
+
export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
|
|
9
|
+
export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
|
|
10
|
+
/** 一层 settings 的上限。一份 CC settings 是人手写的 JSON,不是数据集;16 KiB 宽到装得下几百条 allow,
|
|
11
|
+
* 窄到一次请求不能拿它当上传通道。层数同理(core v1 只读三层,给 3 就够,多给等于给一个放大面)。 */
|
|
12
|
+
const MAX_LAYER_CONTENT_CHARS = 16_384;
|
|
13
|
+
const MAX_LAYERS = 3;
|
|
14
|
+
/** 🔴 边界必 schema(宪法 [2704])。`.strict()`:多给一个键 = 400,不是静默忽略 —— 一个把 `content`
|
|
15
|
+
* 拼错成 `contents` 的客户端应该当场知道,而不是拿到一份「零候选」的预览去纳闷。 */
|
|
16
|
+
const CcImportPrepareBodySchema = z
|
|
17
|
+
.object({
|
|
18
|
+
layers: z
|
|
19
|
+
.array(z
|
|
20
|
+
.object({
|
|
21
|
+
layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
|
|
22
|
+
path: z.string().min(1).max(1024),
|
|
23
|
+
/** 项目层规则的作用域根。**用户自己的**目录标签(它只影响这条规则将来在哪些 cwd 下生效),
|
|
24
|
+
* 故不做**真实性**校验——server 上没有那棵树可对照,假装校验过才是谎。
|
|
25
|
+
* 🔴 但**非空**是硬的(codex 交叉复审 round5 [high] 一,验真后修):`root: ""` 会被 core 直接
|
|
26
|
+
* 变成 `{kind:"project", root:""}`,而空前缀在 `pathWithinRoot` 下**包含每一个 cwd** —— 一条
|
|
27
|
+
* 项目内规则就此悄悄升成全局规则;更糟的是本仓的读面对空 root 是整桶拒,于是一次「成功」的
|
|
28
|
+
* 导入会让这位租户**之后每一次**规则读写都失败。 */
|
|
29
|
+
root: z.string().min(1).max(1024),
|
|
30
|
+
content: z.string().max(MAX_LAYER_CONTENT_CHARS),
|
|
31
|
+
})
|
|
32
|
+
.strict())
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(MAX_LAYERS),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
const CcImportRedeemBodySchema = z.object({ ticket: z.string().min(1).max(190) }).strict();
|
|
38
|
+
export async function handleRules(req, res, url, ctx) {
|
|
39
|
+
const miss = { fell: false };
|
|
40
|
+
await handleRulesBody(req, res, url, ctx, miss);
|
|
41
|
+
return !miss.fell;
|
|
42
|
+
}
|
|
43
|
+
async function handleRulesBody(req, res, url, ctx, miss) {
|
|
44
|
+
const { deps } = ctx;
|
|
45
|
+
const { readJson, rateLimited } = ctx.helpers;
|
|
46
|
+
const isPrepare = req.method === "POST" && url === RULES_CC_IMPORT_PREPARE_PATH;
|
|
47
|
+
const isRedeem = req.method === "POST" && url === RULES_CC_IMPORT_REDEEM_PATH;
|
|
48
|
+
if (!isPrepare && !isRedeem) {
|
|
49
|
+
miss.fell = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// 🔴 `prepare` 每次调用**都会**落一条 pending 审批记录 + 一张票 —— 它是本域唯一一条「一次请求 = 一行」
|
|
53
|
+
// 的写面,没有限流就是一条行洪口。与 `POST /v1/tasks/:id/asks/:askId/decision` 同姿势(限流在最前)。
|
|
54
|
+
if (rateLimited(req, res))
|
|
55
|
+
return;
|
|
56
|
+
// 鉴权**先于**可用性(501-before-auth 先例的反面同理):这两口没有可被探测的部署事实,但
|
|
57
|
+
// principal 是本域的全部授权基础,先立住它再谈别的。
|
|
58
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
59
|
+
if (principal === undefined) {
|
|
60
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const lane = deps.ruleConsent;
|
|
64
|
+
if (lane === undefined) {
|
|
65
|
+
sendError(res, 501, "capability.rule_store_required", "this deployment has no permission-rule store wired");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let rawBody;
|
|
69
|
+
try {
|
|
70
|
+
rawBody = await readJson(req);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
sendError(res, 400, "request.body_shape", "body must be valid JSON");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isPrepare) {
|
|
77
|
+
const parsed = CcImportPrepareBodySchema.safeParse(rawBody);
|
|
78
|
+
if (!parsed.success) {
|
|
79
|
+
sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const prepared = await lane.prepareImport(principal, parsed.data.layers);
|
|
83
|
+
if (!prepared.ok) {
|
|
84
|
+
// 413(不是 400):族语义就是「体/条目超上限」,与附件/图片同码同状态(附录 A 已成文)。
|
|
85
|
+
// 帽的位置与理由(为什么单层字节帽不够、为什么帽在**铸票之前**)见 `rules-consent.ts` 的
|
|
86
|
+
// `MAX_IMPORT_CANDIDATES`。
|
|
87
|
+
sendError(res, 413, "request.payload_too_large", "too many allow entries across the submitted settings layers");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
sendJson(res, 200, { preview: prepared.preview, ticket: prepared.ticket, expiresAtMs: prepared.expiresAtMs });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const parsed = CcImportRedeemBodySchema.safeParse(rawBody);
|
|
94
|
+
if (!parsed.success) {
|
|
95
|
+
sendError(res, 400, "request.body_shape", "body must be {ticket}");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const redeemed = await lane.redeemImport(principal, parsed.data.ticket);
|
|
99
|
+
if (!redeemed.ok) {
|
|
100
|
+
logger.info("rule import ticket refused", { reason: redeemed.reason, detail: redeemed.detail, retryable: redeemed.retryable === true });
|
|
101
|
+
// 🔴 codex 交叉复审 round2 [medium](验真后修):**可重试**那一支不能折进 404。
|
|
102
|
+
// 为什么这不破坏零存在性 oracle:走到这一支的调用方**已经赢下了一次 principal 绑定的认领** ——
|
|
103
|
+
// 也就是说他已经证明了这张票是他自己的、没过期、没用过。对他说「稍后再试」泄不出任何**别人**的
|
|
104
|
+
// 存在性(伪造 / 别人的 / 过期 / 已用过 四类仍然同形 404,一个字都没变)。反过来把它折成 404 才是
|
|
105
|
+
// 真损失:round1 修好的「认领已放回、这张票还能用」在 wire 上被抹掉,客户端按常规 404 处置一丢了之。
|
|
106
|
+
if (redeemed.retryable === true) {
|
|
107
|
+
res.setHeader("retry-after", String(RULE_IMPORT_RETRY_AFTER_SEC)); // 体里有等待提示 ⇒ 标准头同值(同 503 族先例)
|
|
108
|
+
sendError(res, 503, "state.rule_import_retry", "the rule import could not be completed right now; the ticket is still usable — retry", { retryAfterSec: RULE_IMPORT_RETRY_AFTER_SEC });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// 同形 404(顶注):四类成因**不上 wire**,只进服务端日志。
|
|
112
|
+
sendError(res, 404, "not_found.rule_ticket", "no such import ticket");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
sendJson(res, 200, { result: redeemed.result });
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/177 —— 共享记忆库(org 盘)的**只读 HTTP 面**。
|
|
3
|
+
*
|
|
4
|
+
* 路由:`GET /v1/shared-memory/stores`、`GET /v1/shared-memory/stores/:store/documents`、
|
|
5
|
+
* `GET /v1/shared-memory/stores/:store/document?path=`。本域零可变状态。
|
|
6
|
+
*
|
|
7
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
* 挂载条件 = store ∧ org 折叠面,**双在场**才有这个域(v2/F4 硬化)
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* 供给面在场但授权面缺席时,这个域整体**不挂载** —— 请求落到全局 404 `not_found.route`。刻意不是 501:
|
|
11
|
+
* · 501 的自述是「这个面存在,只是本部署没开」,它对一个连身份都无法被判定的调用方泄露了「这套部署
|
|
12
|
+
* 有共享记忆库」,并暗示补个配置就能读;
|
|
13
|
+
* · 「store 好了先挂载、授权后补」是这条纪律真正要堵的中间态 —— 一个没有成员性判据的读面,唯一能做的
|
|
14
|
+
* 要么是全放(跨租户读)要么是全拒(伪装成故障),两个都比「这个面不存在」差。
|
|
15
|
+
* 能力广告(`capabilities.sharedMemory`)读的是**同一个合取式**,所以「说 yes ⟺ 面真能用」结构上成立。
|
|
16
|
+
*
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
* 授权:一次 snapshot,零第二真源
|
|
19
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
* 每个请求取**一次** `provider.snapshot({ principal })` —— 与模型面(core 的 memory_list/memory_read)
|
|
21
|
+
* 走的是同一个供给口、同一次绑定。可见的 store 集合因此按定义等于模型看到的那一份,HTTP 面不可能比
|
|
22
|
+
* 模型面多看见一个库。越界一律 404 且**与「这个库根本不存在」字节同形**:状态码、errorCode、message
|
|
23
|
+
* 全同,差一个字就是一次成员性探针(某人能用 404 的差异枚举出别的 org 有哪些库)。
|
|
24
|
+
*
|
|
25
|
+
* 文档**不存在**则另说(`not_found.shared_memory_document`):调用方已经证明了自己是这个库的成员,
|
|
26
|
+
* 「库里没有这份文件」对他不是秘密,把它和「你不是成员」合并反而会让成员无法区分打错路径和权限问题。
|
|
27
|
+
*/
|
|
28
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
29
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
30
|
+
export declare function handleSharedMemory(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
31
|
+
//# sourceMappingURL=shared-memory.d.ts.map
|