@sema-agent/server 7.80.0 → 7.80.2
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/boot/runner-deps.d.ts +17 -1
- package/dist/boot/runner-deps.js +22 -3
- package/dist/boot/stage-07-capability-layer.js +2 -1
- package/dist/capabilities/skills.d.ts +10 -0
- package/dist/capabilities/skills.js +3 -1
- package/dist/http/admission.js +3 -1
- package/dist/http/resume-legs.js +4 -3
- package/dist/http/routes/admin-drain.js +1 -1
- package/dist/http/routes/admin-memory-optout.js +1 -1
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/notify-wake.js +1 -1
- package/dist/http/routes/run-memory-capture.js +2 -2
- package/dist/http/routes/runs.js +8 -8
- package/dist/http/routes/session-sync.js +1 -1
- package/dist/http/wire-types.d.ts +15 -9
- package/dist/observability/metrics.js +1 -1
- package/dist/parked-decide.d.ts +9 -0
- package/dist/parked-decide.js +3 -3
- package/dist/run-local.js +2 -1
- package/dist/spec-fields.js +33 -30
- package/dist/task-cwd.d.ts +21 -0
- package/dist/task-cwd.js +3 -0
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.d.ts +20 -0
- package/dist/trace/project.js +15 -1
- package/package.json +2 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (`runnerTierFrozen` 判据就取自构造那一刻的 `config.tiers`)。
|
|
10
10
|
* `getRunStore()` 是唯一的晚绑取值(runStore 在本段之后才构造),原文靠同作用域前向引用。
|
|
11
11
|
*/
|
|
12
|
-
import { type RunnerDeps, type WorkflowRunStore, type AskOutcome } from "@sema-agent/core";
|
|
12
|
+
import { type CacheBreakFinding, type RunnerDeps, type WorkflowRunStore, type AskOutcome } from "@sema-agent/core";
|
|
13
13
|
import type { createBrain } from "../brain.js";
|
|
14
14
|
import type { buildPricing, createTracer } from "../budget.js";
|
|
15
15
|
import type { ServiceConfig } from "../config.js";
|
|
@@ -60,6 +60,22 @@ export declare function createRunnerDepsOnAsk(toolApproval: ToolApprovalCoordina
|
|
|
60
60
|
* `classificationKnown:false`(指标标签折 `unknown`),不吞不编;与安装包的锁步由 test/boot-runner-deps-shared-base.test.ts
|
|
61
61
|
* 的 dist 扫描格钉住(core 加词/收进 d.ts 那天先红)。 */
|
|
62
62
|
export declare const CONFIG_ADVISORY_CLASSIFICATIONS: ReadonlySet<string>;
|
|
63
|
+
/** S-386:core 的 [ref] 前缀缓存**断裂**探测器交给宿主的根因**闭集**,以及每个词的日志级别。
|
|
64
|
+
* 闭集**不抄词**:词源直接取 core 公开导出的 `CacheBreakFinding["cause"]`(`@sema-agent/core` 根导出,
|
|
65
|
+
* `dist/index.d.ts:128`;实现在 `dist/core/cache-break-detector.d.ts`),于是 core 加词/删词 ⇒
|
|
66
|
+
* `Record<PromptCacheBreakCause, …>` 少键/多键 = **编译错**(CLAUDE.md [ref]「词表是闭集 + 穷举」)。
|
|
67
|
+
* ⚠️ 首版在这里**手抄**了五个字面量,并在本注与 CHANGELOG 里声称「core 加词 = 编译错」—— 复审实测
|
|
68
|
+
* 证伪(给 core d.ts 加词后 tsc 照绿,只有一格正则扫描会红):抄来的联合与 core 无编译期联系。
|
|
69
|
+
* 运行期收到词表外的值(`ctx.classification` 在 core 的 onError 契约上只是 `string`)走最响的那一档
|
|
70
|
+
* (error)并标 `classificationKnown:false`,退化方向永远是**多一条告警**。
|
|
71
|
+
*
|
|
72
|
+
* 🔴 定级判据是**事实**,不是 provider 名单:`dist/core/cache-break-detector.js` 的 `observe()` 里
|
|
73
|
+
* `const before = prev.cacheRead; if (before <= 0) return undefined` —— 探测器只有在**上一轮真的从
|
|
74
|
+
* 前缀缓存读到过 token** 时才会给出 finding。所以「带 classification」这件事本身就证明了「这条路由
|
|
75
|
+
* 在服务前缀缓存」,前缀 bug 四词(model-switch / tool-schema / tool-set / system-prefix)落 error
|
|
76
|
+
* 是有据的;`server-or-ttl` 在 agentic 任务里是常态(慢工具 ⇒ 5min+ 间隔,provider 缓存到期)⇒ warn。 */
|
|
77
|
+
export type PromptCacheBreakCause = CacheBreakFinding["cause"];
|
|
78
|
+
export declare const PROMPT_CACHE_BREAK_CAUSE_LEVEL: Readonly<Record<PromptCacheBreakCause, "error" | "warn">>;
|
|
63
79
|
export interface RunnerDepsCtx {
|
|
64
80
|
config: ServiceConfig;
|
|
65
81
|
logger: Logger;
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -24,6 +24,14 @@ export const CONFIG_ADVISORY_CLASSIFICATIONS = new Set([
|
|
|
24
24
|
"shared-memory-not-mounted",
|
|
25
25
|
"shell-gate-off",
|
|
26
26
|
]);
|
|
27
|
+
export const PROMPT_CACHE_BREAK_CAUSE_LEVEL = {
|
|
28
|
+
"model-switch": "error",
|
|
29
|
+
"tool-schema": "error",
|
|
30
|
+
"tool-set": "error",
|
|
31
|
+
"system-prefix": "error",
|
|
32
|
+
"server-or-ttl": "warn",
|
|
33
|
+
};
|
|
34
|
+
const PROMPT_CACHE_BREAK_LEVELS = new Map(Object.entries(PROMPT_CACHE_BREAK_CAUSE_LEVEL));
|
|
27
35
|
const CEILING_ORIGINS = new Set(["walltime", "turns"]);
|
|
28
36
|
export function createEngineNoticeForwarder(logger, router = defaultEngineNoticeRouter, metrics) {
|
|
29
37
|
return (notice) => {
|
|
@@ -224,9 +232,20 @@ export function createRunnerDeps(ctx) {
|
|
|
224
232
|
return;
|
|
225
233
|
}
|
|
226
234
|
if (ctx.phase === "prompt-cache") {
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
235
|
+
const classification = ctx.classification;
|
|
236
|
+
const causeLevel = classification === undefined ? undefined : PROMPT_CACHE_BREAK_LEVELS.get(classification);
|
|
237
|
+
const level = classification === undefined ? "warn" : (causeLevel ?? "error");
|
|
238
|
+
const fields = {
|
|
239
|
+
sessionId: ctx.sessionId,
|
|
240
|
+
confirmed: classification !== undefined,
|
|
241
|
+
...(classification !== undefined ? { classification, classificationKnown: causeLevel !== undefined } : {}),
|
|
242
|
+
err: String(err),
|
|
243
|
+
};
|
|
244
|
+
metrics.inc("prompt_cache_low_hit_total", {
|
|
245
|
+
...(classification !== undefined ? { classification: causeLevel !== undefined ? classification : "unknown" } : {}),
|
|
246
|
+
level,
|
|
247
|
+
});
|
|
248
|
+
if (level === "warn")
|
|
230
249
|
logger.warn("prompt_cache_break", fields);
|
|
231
250
|
else
|
|
232
251
|
logger.error("prompt_cache_break", fields);
|
|
@@ -8,6 +8,7 @@ import { webSearchConfigFromEnv, createWebSearchBackend, shouldProbeWebSearchOnB
|
|
|
8
8
|
import { createAuthorizer } from "../security.js";
|
|
9
9
|
import { assertGateIntentServiceable, buildForceDurableGateInertNotice, hasOperatorGateIntent } from "../approval.js";
|
|
10
10
|
import { loadSkills } from "../capabilities/skills.js";
|
|
11
|
+
import { toolsRunOnThisHost } from "../task-cwd.js";
|
|
11
12
|
import { createRepoClient } from "../capabilities/repo-tools.js";
|
|
12
13
|
import { buildScenarios, builtinScenarioDetails } from "../capabilities/scenarios.js";
|
|
13
14
|
import { RateLimiter } from "../observability/rate-limit.js";
|
|
@@ -108,7 +109,7 @@ export async function runStage07(ctx) {
|
|
|
108
109
|
? backend.rateLimiter(config.rateLimitPerMin, counterDegradeHook).startRefresh()
|
|
109
110
|
: new RateLimiter(config.rateLimitPerMin);
|
|
110
111
|
bindRateLimiter(rateLimiter);
|
|
111
|
-
let skills = loadSkills(config.skillsDir);
|
|
112
|
+
let skills = loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
|
|
112
113
|
skills = await configCenter.applyCenterCapabilities(skills);
|
|
113
114
|
const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
|
|
114
115
|
const webSearchCfg = webSearchConfigFromEnv();
|
|
@@ -41,6 +41,16 @@ export interface LoadedSkill {
|
|
|
41
41
|
export interface LoadSkillsOptions {
|
|
42
42
|
/** 不可信 lane 的约束根(通常=clone root)。缺席=受控 lane,不做逐条目约束。 */
|
|
43
43
|
confineTo?: string;
|
|
44
|
+
/**
|
|
45
|
+
* S-377 —— 「**工具跑的那台机器**的盘就是本进程这台盘吗」。真 ⇒ 目录形态技能带上自己的目录
|
|
46
|
+
* (`SkillSpec.baseDir`);假/缺席 ⇒ **不带**。判据的单一属主 = `task-cwd.ts` 的
|
|
47
|
+
* {@link import("../task-cwd.js").toolsRunOnThisHost}(调用方按它算,别在这里第二次判 lane)。
|
|
48
|
+
*
|
|
49
|
+
* 🔴 **缺席 = 不带**,是**故意的 fail-closed 方向**:带了一条工具那边不存在的路径,伤是「模型按它去
|
|
50
|
+
* 读、扑空,顺带看到本机目录布局」;不带的伤只是少一条线索(附件本来就随 invoke 全文内联投递,
|
|
51
|
+
* core 的律也明写缺席 ⇒ 结果逐字节与 baseDir 到货之前相同)。两个方向不对称 ⇒ 默认取轻的那边。
|
|
52
|
+
*/
|
|
53
|
+
toolsShareThisFilesystem?: boolean;
|
|
44
54
|
}
|
|
45
55
|
/** `p` 的 realpath 必须落在 `rootReal` 之内(含自身),否则抛。`sep` 边界判定防 `/a/root-evil` 撞
|
|
46
56
|
* `/a/root` 前缀。目标不存在 ⇒ realpath 抛 ENOENT,同样是拒绝(悬空链接不该被读)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { join, sep } from "node:path";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
3
|
export function assertConfined(p, rootReal, label = "skill entry") {
|
|
4
4
|
let real;
|
|
5
5
|
try {
|
|
@@ -59,6 +59,8 @@ export function loadSkills(dir, opts) {
|
|
|
59
59
|
const files = collectAttachments(skillDir, "", confineRoot);
|
|
60
60
|
if (files.length > 0)
|
|
61
61
|
skill.spec.files = files;
|
|
62
|
+
if (opts?.toolsShareThisFilesystem === true)
|
|
63
|
+
skill.spec.baseDir = resolve(skillDir);
|
|
62
64
|
push(skill, join(skillDir, "SKILL.md"));
|
|
63
65
|
}
|
|
64
66
|
}
|
package/dist/http/admission.js
CHANGED
|
@@ -7,7 +7,7 @@ import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
|
7
7
|
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
8
8
|
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
9
9
|
import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
10
|
-
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
10
|
+
import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
|
|
11
11
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
12
12
|
import { sendError, httpErrorCode } from "./send.js";
|
|
13
13
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
@@ -27,6 +27,8 @@ export function validateUserSkills(skills) {
|
|
|
27
27
|
return "skill.description must be a string";
|
|
28
28
|
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
29
29
|
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)`;
|
|
30
|
+
if (s["baseDir"] !== undefined && !isValidCwd(s["baseDir"]))
|
|
31
|
+
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)`;
|
|
30
32
|
}
|
|
31
33
|
return null;
|
|
32
34
|
}
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -1031,16 +1031,17 @@ export function createResumeLegs(ctx) {
|
|
|
1031
1031
|
async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
|
|
1032
1032
|
const cs = deps.checkpointStore;
|
|
1033
1033
|
if (!deps.runStore) {
|
|
1034
|
-
return { status: 501, body: { error: "delivering
|
|
1034
|
+
return { status: 501, body: { error: "delivering this decision requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
|
|
1035
1035
|
}
|
|
1036
1036
|
const ctx = await cs.getCtx(sessionId).catch(() => null);
|
|
1037
1037
|
if (!ctx) {
|
|
1038
1038
|
return {
|
|
1039
1039
|
status: 409,
|
|
1040
1040
|
body: {
|
|
1041
|
-
error: "
|
|
1041
|
+
error: "this approval waited too long to be delivered: the session this run was started from no longer keeps what a new turn would need, because that state is dropped once the session has been idle past this deployment's retention window (REAP_RUN_STALE_SEC — its current value is staleAfterSec below). Nothing was decided and the approval is still pending: resume the run yourself with the runId below, or ask whoever operates this deployment to raise REAP_RUN_STALE_SEC so approvals can sit longer before someone decides them",
|
|
1042
1042
|
errorCode: "conflict.resume_context_unavailable",
|
|
1043
1043
|
runId,
|
|
1044
|
+
staleAfterSec: deps.config.runStaleSec,
|
|
1044
1045
|
},
|
|
1045
1046
|
};
|
|
1046
1047
|
}
|
|
@@ -1108,7 +1109,7 @@ export function createResumeLegs(ctx) {
|
|
|
1108
1109
|
}
|
|
1109
1110
|
const gateKind = cp.gate?.kind;
|
|
1110
1111
|
if (gateKind !== "task_done") {
|
|
1111
|
-
return { status: 409, body: { error: `cannot wake: this
|
|
1112
|
+
return { status: 409, body: { error: `cannot wake: this session is waiting on a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
|
|
1112
1113
|
}
|
|
1113
1114
|
const trusted = operator;
|
|
1114
1115
|
if (message !== undefined) {
|
|
@@ -24,7 +24,7 @@ export async function handleAdminDrain(req, res, match, ctx) {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
if (!deps.drainState) {
|
|
27
|
-
sendError(res, 501, "capability.drain_state_required", "this deployment has no drain state wired (embedded
|
|
27
|
+
sendError(res, 501, "capability.drain_state_required", "this deployment has no drain state wired (embedded in another process?) — the declared reason would have nowhere to surface");
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
deps.drainState.reason = reason.trim();
|
|
@@ -23,7 +23,7 @@ export async function handleMemoryOptOut(req, res, match, ctx) {
|
|
|
23
23
|
const operator = principal;
|
|
24
24
|
const store = deps.memoryOptOutGrant;
|
|
25
25
|
if (store === undefined) {
|
|
26
|
-
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 forgetting
|
|
26
|
+
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");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (target !== undefined && (target === null || memoryOptOutPrincipalShapeProblem(target) !== undefined)) {
|
|
@@ -501,7 +501,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
|
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
if (!deps.approvalExemptionStore) {
|
|
504
|
-
sendError(res, 501, "capability.store_required", "approval exemptions need a store backend (DB_BACKEND
|
|
504
|
+
sendError(res, 501, "capability.store_required", "approval exemptions need a store backend (DB_BACKEND=mysql|pg|local)");
|
|
505
505
|
return;
|
|
506
506
|
}
|
|
507
507
|
if (!body.checkpointToken && !body.boundCallId) {
|
|
@@ -92,7 +92,7 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
if (!deps.workflowCompletionInbox) {
|
|
95
|
-
sendError(res, 501, "capability.workflow_store_required", "external notify
|
|
95
|
+
sendError(res, 501, "capability.workflow_store_required", "external notify requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
|
|
@@ -72,7 +72,7 @@ export async function handleRunMemoryCapture(req, res, match, ctx) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
if (isParkedRunStatus(run.status)) {
|
|
75
|
-
sendError(res, 409, "steering.not_running", "run is
|
|
75
|
+
sendError(res, 409, "steering.not_running", "run is waiting on a pending decision — the memory-capture opt-out verb needs a live turn, and a resumed continuation rebuilds its spec from the persisted body (it cannot take a new declaration); the opt-out becomes effective on this session's next NEW submit (POST /v1/tasks with this sessionId and memoryCapture: 'off') — content captured until then stays captured");
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
if (isTerminalRunStatus(run.status)) {
|
|
@@ -84,7 +84,7 @@ export async function handleRunMemoryCapture(req, res, match, ctx) {
|
|
|
84
84
|
sendError(res, 409, "steering.not_running", "run is live on another replica — the memory-capture opt-out verb must reach the replica holding the stream; resend it there, or declare memoryCapture: 'off' on the next submit");
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
sendError(res, 409, "steering.not_running", "no live steerable face for this run on this replica — verify
|
|
87
|
+
sendError(res, 409, "steering.not_running", "no live steerable face for this run on this replica — a verify or cascade submission exposes none; declare memoryCapture: 'off' on the next submit of this session instead");
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
default:
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -350,7 +350,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
350
350
|
continue;
|
|
351
351
|
}
|
|
352
352
|
if (now?.status === "running") {
|
|
353
|
-
sendError(res, 409, "conflict.run_not_running", "the cancel flag could not be set: this run was not in 'running' state at the moment of each attempt (it keeps flipping between running and a
|
|
353
|
+
sendError(res, 409, "conflict.run_not_running", "the cancel flag could not be set: this run was not in 'running' state at the moment of each attempt (it keeps flipping between running and a pending-decision state; the `status` below is the word the row actually had) — retry cancel", { taskId, status: now.status });
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
356
|
sendJson(res, 202, { taskId, status: now?.status ?? "failed", cancelRequested: now?.cancelRequested ?? false, note: "run already terminal — cancel is a no-op" });
|
|
@@ -528,7 +528,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
528
528
|
sendDuplicateKey();
|
|
529
529
|
return;
|
|
530
530
|
}
|
|
531
|
-
sendNotRunning("run is no longer
|
|
531
|
+
sendNotRunning("run is no longer waiting on a pending decision (resolved or expired)");
|
|
532
532
|
return;
|
|
533
533
|
}
|
|
534
534
|
if (run.status === "running") {
|
|
@@ -537,7 +537,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
537
537
|
sendNotRunning("run is live on another replica — a live steer must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
|
|
538
538
|
}
|
|
539
539
|
else {
|
|
540
|
-
sendNotRunning("no live steerable face for this run on this replica — verify
|
|
540
|
+
sendNotRunning("no live steerable face for this run on this replica — a verify or cascade submission exposes none (those return a result, not a live stream); use POST /v1/runs/:id/cancel for a run-level stop");
|
|
541
541
|
}
|
|
542
542
|
return;
|
|
543
543
|
}
|
|
@@ -637,7 +637,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
637
637
|
return;
|
|
638
638
|
}
|
|
639
639
|
if (mintResult.noResumeCtx) {
|
|
640
|
-
sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer
|
|
640
|
+
sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer queued now could never be delivered by POST /v1/sessions/:id/wake; submit a new task instead");
|
|
641
641
|
return;
|
|
642
642
|
}
|
|
643
643
|
if (mintResult.parked) {
|
|
@@ -723,7 +723,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
723
723
|
const sendInterruptNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
|
|
724
724
|
const sendNoLiveTurn = (status, streamJustSettled) => {
|
|
725
725
|
if (isParkedRunStatus(status)) {
|
|
726
|
-
sendError(res, 409, "interrupt.nothing_in_flight", "run is
|
|
726
|
+
sendError(res, 409, "interrupt.nothing_in_flight", "run is waiting on a pending decision — there is no in-flight turn to interrupt; to deliver input use POST /v1/runs/:id/steer (it waits with the pending decision and is delivered when the run resumes)");
|
|
727
727
|
return;
|
|
728
728
|
}
|
|
729
729
|
if (streamJustSettled) {
|
|
@@ -736,7 +736,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
736
736
|
sendError(res, 409, "interrupt.not_held", "run is live on another replica — interrupt must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
|
|
737
737
|
}
|
|
738
738
|
else {
|
|
739
|
-
sendError(res, 409, "interrupt.not_held", "no live interruptible turn face for this run on this replica — verify
|
|
739
|
+
sendError(res, 409, "interrupt.not_held", "no live interruptible turn face for this run on this replica — a verify or cascade submission exposes none (those return a result, not a live stream); use POST /v1/runs/:id/cancel for a run-level stop");
|
|
740
740
|
}
|
|
741
741
|
return;
|
|
742
742
|
}
|
|
@@ -1004,7 +1004,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1004
1004
|
if (match.decl.id === "run-task-output" || match.decl.id === "run-task-stop") {
|
|
1005
1005
|
const stopVerb = match.decl.id === "run-task-stop";
|
|
1006
1006
|
if (!deps.runStore || !deps.taskHandleOutput || !deps.taskHandleStop) {
|
|
1007
|
-
sendError(res, 501, "capability.run_store_required", "task-handle verbs require the run store
|
|
1007
|
+
sendError(res, 501, "capability.run_store_required", "task-handle verbs require the run store and the task registry");
|
|
1008
1008
|
return;
|
|
1009
1009
|
}
|
|
1010
1010
|
if (stopVerb && rateLimited(req, res))
|
|
@@ -1259,7 +1259,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1259
1259
|
});
|
|
1260
1260
|
const askStore = gate.active ? gate.askStore : undefined;
|
|
1261
1261
|
if (!askStore) {
|
|
1262
|
-
sendError(res, 501, "feature.approval_ask_disabled", "the durable stream-approval ask ledger is not enabled on this worker (needs STREAM_APPROVAL_ENABLED plus a DURABLE store backend — the local
|
|
1262
|
+
sendError(res, 501, "feature.approval_ask_disabled", "the durable stream-approval ask ledger is not enabled on this worker (needs STREAM_APPROVAL_ENABLED plus a DURABLE store backend — the local backend keeps asks in memory and would lose accepted decisions on restart)");
|
|
1263
1263
|
return;
|
|
1264
1264
|
}
|
|
1265
1265
|
const principal = gatedPrincipal(req, deps.config);
|
|
@@ -457,7 +457,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
457
457
|
if (pending.resolution !== undefined && overwriteWipeIncapable(deps.sessionPolicyStore)) {
|
|
458
458
|
await pending.handle.abort().catch(() => undefined);
|
|
459
459
|
cleanupStaging(stagingId);
|
|
460
|
-
sendError(res, 501, "capability.policy_purge_required", "overwrite-dst cannot proceed: this deployment's session-policy store has no deleteBySession purge
|
|
460
|
+
sendError(res, 501, "capability.policy_purge_required", "overwrite-dst cannot proceed: this deployment's session-policy store has no deleteBySession purge verb, so the destination's stale policy rows cannot be wiped and the import would land a MIXED state (the source's conversation plus the destination's leftover policy rows, which keep gating). Upgrade the policy store (every in-tree store carries deleteBySession since core 1.423), or re-run without resolution overwrite-dst");
|
|
461
461
|
return;
|
|
462
462
|
}
|
|
463
463
|
try {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
|
|
7
7
|
* sibling request/response shapes can migrate here later on the same pattern.
|
|
8
8
|
*/
|
|
9
|
-
import type { A2aServerSpec, AgentDefinition, McpServerSpec } from "@sema-agent/core";
|
|
9
|
+
import type { A2aServerSpec, AgentDefinition, McpServerSpec, SkillSpec } from "@sema-agent/core";
|
|
10
10
|
export interface TaskRequestBody {
|
|
11
11
|
objective: string;
|
|
12
12
|
sessionId?: string;
|
|
@@ -98,14 +98,20 @@ export interface TaskRequestBody {
|
|
|
98
98
|
jobId?: string;
|
|
99
99
|
/** Personalization seam: the caller's OWN skills for this task, threaded into core's
|
|
100
100
|
* `TaskSpec.skills`. Merged AFTER the scenario's skills with SCENARIO-WINS on a name clash (the security
|
|
101
|
-
* baseline can never be shadowed by user content — see mergeUserSkills).
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
* baseline can never be shadowed by user content — see mergeUserSkills). User skills are user-authored
|
|
102
|
+
* content in the user's OWN principal-isolated task (self-harm surface, not privilege widening);
|
|
103
|
+
* scenario/center skills remain the only trusted layer.
|
|
104
|
+
*
|
|
105
|
+
* 🔴 **类型 = core 的 `SkillSpec` 本体,不是它的手抄子集**(S-377):这里曾写死
|
|
106
|
+
* `{ name; description; content }` —— core 闭集六位里的三位,而 `manifest` / `files` / `baseDir`
|
|
107
|
+
* 三位在运行期一直是**收的**(受理门 `validateUserSkills` 不判未知键,`mergeUserSkills` 原样透传到
|
|
108
|
+
* `TaskSpec.skills`)。一份会漂的手抄 = 声明面比真受理面**窄**,SDK 消费方按声明写代码就以为发不了,
|
|
109
|
+
* 而 core 加键(7.20.0 [ref] 的 `baseDir`)时这里连红都不会红。改成 core 本体后,闭集由 tsc 看守。
|
|
110
|
+
* ⚠️ 受理**判据**(形/尺寸)仍在 `validateUserSkills` + `spec-fields.ts` 的 `SKILL_FIELD_SHAPES`——
|
|
111
|
+
* 类型说「哪些键存在」,门说「值合不合法」,两者不是一件事。
|
|
112
|
+
* (旧注里的「Caps: ≤10 skills, content ≤32KB each」两条帽早已随 [ref] / core 1.293 退役,一并纠正:
|
|
113
|
+
* 今天只剩 `content ≤ 1 MiB` 这一条 core 加载门孪生。) */
|
|
114
|
+
skills?: SkillSpec[];
|
|
109
115
|
/** §7 P0.5 (用户按需选像): the sandbox image PROFILE this task wants (e.g.
|
|
110
116
|
* "code-full" / "code-python"). An INTENT, never a digest — the service resolves it to a published digest with
|
|
111
117
|
* the caller's principal (fail-closed visibility re-admit) and applies it per-pod. Only supported on the k8s
|
|
@@ -206,7 +206,7 @@ export function createMetrics() {
|
|
|
206
206
|
m.histogram("task_cache_hit_rate", "Prefix-cache hit rate per task (core-computed stats.cacheHitRate, 0..1)", [
|
|
207
207
|
0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 1,
|
|
208
208
|
]);
|
|
209
|
-
m.counter("prompt_cache_low_hit_total", "
|
|
209
|
+
m.counter("prompt_cache_low_hit_total", "Prefix-cache advisories the Runner handed the host, by {classification,level} (S-386: the design/31 break causes carry a classification; the task-end low-hit heuristic and the two cache-family wiring advisories carry none)");
|
|
210
210
|
m.counter("model_cost_micro_usd_total", "Authoritative model spend (integer micro-USD) over all brain calls, by model");
|
|
211
211
|
m.histogram("brain_first_token_ms", "Time to first content token per brain call (gateway-hang signal)", [
|
|
212
212
|
50, 100, 250, 500, 1000, 2000, 5000, 10000, 30000,
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -260,6 +260,15 @@ export type WorkflowParkedDecidePlan = {
|
|
|
260
260
|
* 三句里持久数据那一句要的就是这个形:响亮拒,不做静默双读(读 `agents[]` 补一份身份正是被退役的双算)。
|
|
261
261
|
*
|
|
262
262
|
* 体上只带 `runId`(那是操作员唯一用得上的坐标;真源都读不出来,任何计数都是编的)。
|
|
263
|
+
*
|
|
264
|
+
* 🔴 **S-376(词域门,7.80.1):拒文逐字改,status / errorCode / 体键零变。** 旧句把本注里的内部机件整段
|
|
265
|
+
* 搬上了 wire(「the parks the run owns」「when it parks」「join your checkpoint to a park」「its own parked
|
|
266
|
+
* legs」)—— 那些名字调用方一个都拿不到。新句把承重的四件事一件不少地留着(不投递 / 卡仍 pending 什么都没
|
|
267
|
+
* 伪造 / 恢复动作 `Workflow({resumeFromRunId})` / 只有引擎也拒才是真恢复不了)。
|
|
268
|
+
* ⚠️ **新句刻意不提 `checkpointToken`**(codex 对抗复审 r1 指出、亲核属实):契约 D-1 逐字写着它是
|
|
269
|
+
* server-INTERNAL、不上 `/v1/approvals` 记录、合规 SDK 绝不发送 —— 让调用方去找一个它既没发也拿不到的
|
|
270
|
+
* 字段是错指路。新句说的是「无法确认这项待决审批属于这条 run」。判据与冻结钉见
|
|
271
|
+
* `test/api-error-text-lexicon.test.ts`。
|
|
263
272
|
*/
|
|
264
273
|
export declare function denyWorkflowParksUnreadable(origin: Extract<ParkOrigin, {
|
|
265
274
|
kind: "workflow_parks_unreadable";
|
package/dist/parked-decide.js
CHANGED
|
@@ -89,7 +89,7 @@ export function denyWorkflowParksUnreadable(origin) {
|
|
|
89
89
|
return {
|
|
90
90
|
status: 409,
|
|
91
91
|
body: {
|
|
92
|
-
error: "this
|
|
92
|
+
error: "this decision cannot be matched to the pending step it belongs to: this run's record was written by an older engine, or one of its entries is damaged, so it no longer says which pending steps the run owns — and that list is the only proof that the approval you are deciding belongs to this run. The decision is therefore NOT delivered, rather than answered with a receipt nothing can redeem: the approval card stays pending and nothing is forged. Resume the run itself instead — `Workflow({resumeFromRunId})` with the runId below — and the engine rebuilds that list from its own records as it admits the resume, after which deciding here works normally. Only if the engine also refuses that resume is the run truly unrecoverable",
|
|
93
93
|
errorCode: "decide.workflow_parks_unreadable",
|
|
94
94
|
runId: origin.runId,
|
|
95
95
|
},
|
|
@@ -102,7 +102,7 @@ export function planWorkflowParkedDecide(origin, req) {
|
|
|
102
102
|
deny: {
|
|
103
103
|
status: 400,
|
|
104
104
|
body: {
|
|
105
|
-
error: "remember:\"session\" is not supported for
|
|
105
|
+
error: "remember:\"session\" is not supported for this approval: accepting it here only means the decision was handed on for delivery, not that the engine has applied it — an exemption granted now could outlive a decision the engine later rejects. Re-issue the decide without `remember`; the decision itself is accepted normally that way.",
|
|
106
106
|
errorCode: "decide.workflow_remember_unsupported",
|
|
107
107
|
runId: origin.runId,
|
|
108
108
|
},
|
|
@@ -116,7 +116,7 @@ export function planWorkflowParkedDecide(origin, req) {
|
|
|
116
116
|
deny: {
|
|
117
117
|
status: 409,
|
|
118
118
|
body: {
|
|
119
|
-
error: "this
|
|
119
|
+
error: "this run was started on its own rather than from a session, so this deployment has nowhere to deliver the decision. Nothing was decided and the approval is still pending — resume the run yourself with the runId below and decide it there",
|
|
120
120
|
errorCode: "decide.workflow_host_unknown",
|
|
121
121
|
runId: origin.runId,
|
|
122
122
|
},
|
package/dist/run-local.js
CHANGED
|
@@ -23,6 +23,7 @@ import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
|
23
23
|
import { executionLaneHomeDir } from "./execution-lane-caps.js";
|
|
24
24
|
import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
|
|
25
25
|
import { loadSkills } from "./capabilities/skills.js";
|
|
26
|
+
import { toolsRunOnThisHost } from "./task-cwd.js";
|
|
26
27
|
import { createRepoClient } from "./capabilities/repo-tools.js";
|
|
27
28
|
import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
|
|
28
29
|
import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
|
|
@@ -424,7 +425,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
424
425
|
const subRunner = new Runner(subRunnerDeps);
|
|
425
426
|
const handslessRunner = new Runner(withoutExecutionEnv(runnerDeps));
|
|
426
427
|
const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
|
|
427
|
-
const skills = loadSkills(config.skillsDir);
|
|
428
|
+
const skills = loadSkills(config.skillsDir, { toolsShareThisFilesystem: toolsRunOnThisHost(config) });
|
|
428
429
|
const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
|
|
429
430
|
const webSearchCfg = webSearchConfigFromEnv();
|
|
430
431
|
const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
|
package/dist/spec-fields.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isThinkingLevel } from "@sema-agent/core";
|
|
2
2
|
import { taskWallClockSec } from "./task-workflow.js";
|
|
3
|
+
import { isValidCwd } from "./task-cwd.js";
|
|
3
4
|
export function normalizeSuggestNextPrompts(v) {
|
|
4
5
|
if (v === true)
|
|
5
6
|
return true;
|
|
@@ -65,45 +66,47 @@ const toolList = (v) => Array.isArray(v) &&
|
|
|
65
66
|
v.length <= MAX_AGENT_TOOLS &&
|
|
66
67
|
v.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_AGENT_TOOL_NAME_CHARS);
|
|
67
68
|
const plainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
68
|
-
const
|
|
69
|
-
if (!plainObject(
|
|
69
|
+
const skillManifestOk = (m) => {
|
|
70
|
+
if (!plainObject(m))
|
|
70
71
|
return false;
|
|
71
|
-
for (const k of Object.keys(
|
|
72
|
-
if (k !== "
|
|
72
|
+
for (const k of Object.keys(m)) {
|
|
73
|
+
if (k !== "allowTools" && k !== "allowPaths" && k !== "egress" && k !== "lineageId")
|
|
73
74
|
return false;
|
|
74
75
|
}
|
|
75
|
-
if (
|
|
76
|
+
if (!(Array.isArray(m.allowTools) && m.allowTools.every((x) => typeof x === "string")))
|
|
76
77
|
return false;
|
|
77
|
-
if (
|
|
78
|
+
if (m.allowPaths !== undefined && !(Array.isArray(m.allowPaths) && m.allowPaths.every((x) => typeof x === "string")))
|
|
78
79
|
return false;
|
|
79
|
-
if (
|
|
80
|
+
if (m.egress !== undefined && typeof m.egress !== "boolean")
|
|
80
81
|
return false;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
return typeof m.lineageId === "string" && m.lineageId.length > 0;
|
|
83
|
+
};
|
|
84
|
+
const skillFilesOk = (v) => Array.isArray(v) &&
|
|
85
|
+
v.every((f) => plainObject(f) && typeof f.path === "string" && f.path.length > 0 && typeof f.content === "string");
|
|
86
|
+
const SKILL_FIELD_SHAPES = {
|
|
87
|
+
name: { required: true, ok: (v) => typeof v === "string" && v.length > 0 },
|
|
88
|
+
description: { required: true, ok: (v) => typeof v === "string" },
|
|
89
|
+
content: { required: true, ok: (v) => typeof v === "string" && v.length > 0 && v.length <= MAX_AGENT_SKILL_CONTENT_CHARS },
|
|
90
|
+
manifest: { required: false, ok: skillManifestOk },
|
|
91
|
+
files: { required: false, ok: skillFilesOk },
|
|
92
|
+
baseDir: { required: false, ok: isValidCwd },
|
|
93
|
+
};
|
|
94
|
+
const skillOk = (s) => {
|
|
95
|
+
if (!plainObject(s))
|
|
96
|
+
return false;
|
|
97
|
+
for (const k of Object.keys(s)) {
|
|
98
|
+
if (!Object.hasOwn(SKILL_FIELD_SHAPES, k))
|
|
96
99
|
return false;
|
|
97
100
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!plainObject(f))
|
|
103
|
-
return false;
|
|
104
|
-
if (typeof f.path !== "string" || f.path.length === 0 || typeof f.content !== "string")
|
|
101
|
+
for (const [k, shape] of Object.entries(SKILL_FIELD_SHAPES)) {
|
|
102
|
+
const v = s[k];
|
|
103
|
+
if (v === undefined) {
|
|
104
|
+
if (shape.required)
|
|
105
105
|
return false;
|
|
106
|
+
continue;
|
|
106
107
|
}
|
|
108
|
+
if (!shape.ok(v))
|
|
109
|
+
return false;
|
|
107
110
|
}
|
|
108
111
|
return true;
|
|
109
112
|
};
|
|
@@ -134,7 +137,7 @@ const TASK_AGENT_FIELD_SHAPES = {
|
|
|
134
137
|
allowTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
|
|
135
138
|
denyTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
|
|
136
139
|
skills: {
|
|
137
|
-
desc: "an array of { name, description, content } skill objects (optional manifest/files; files entries need { path, content }) — no count cap; the engine renders the listing within its own byte budget",
|
|
140
|
+
desc: "an array of { name, description, content } skill objects (optional manifest/files/baseDir; files entries need { path, content }; baseDir must be an absolute path with no \"..\" segment) — no count cap; the engine renders the listing within its own byte budget",
|
|
138
141
|
ok: (v) => Array.isArray(v) && v.every(skillOk),
|
|
139
142
|
},
|
|
140
143
|
background: { desc: "a boolean", ok: (v) => typeof v === "boolean" },
|
package/dist/task-cwd.d.ts
CHANGED
|
@@ -52,6 +52,27 @@ export declare function cwdHonored(config: {
|
|
|
52
52
|
};
|
|
53
53
|
requirePrincipal?: boolean;
|
|
54
54
|
}): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* S-377 —— 「**工具跑的那台机器**就是本进程这台吗」。回答的是一个**文件系统同一性**问题,不是权限问题:
|
|
57
|
+
* server 盘上的一条绝对路径(出厂 `SKILLS_DIR` 下某个技能自己的目录)对模型的工具**有没有意义**。
|
|
58
|
+
*
|
|
59
|
+
* · `host`(run-on-this-machine,无容器)⇒ 真;
|
|
60
|
+
* · `undefined`(in-process 单用户 lane)⇒ 真 —— 该 lane 根本没挂手(core 落 `StubExecutionEnv`),
|
|
61
|
+
* 路径对谁都不可达,但它在**文件系统同一性**这条轴上确实是同一台机器,判真让规则只剩一条;
|
|
62
|
+
* · 其余每一条(`k8s` / `e2b` / `ssh` / `adb` / `device` / `local-docker`)⇒ 假:工具的盘在别处,
|
|
63
|
+
* server 自己的路径在那边**不存在**,声明它等于给模型一条指向不存在文件的线索,还顺带把本机目录
|
|
64
|
+
* 布局说了出去。
|
|
65
|
+
*
|
|
66
|
+
* 🔴 **为什么不复用 {@link cwdHonored}**:那一只是**权限**判据(单用户 ∧ host),它的 `requirePrincipal`
|
|
67
|
+
* 那一半防的是「一个租户的输入把 agent 指向另一个租户的文件」。本谓词判的路径不是调用方给的,是**运维
|
|
68
|
+
* 自己烤进镜像**的出厂内容 —— 每个租户拿到的是同一份,没有跨租户面。把租户判据搭进来会让多租户 host
|
|
69
|
+
* 部署平白失去这条线索,那是**用错轴**(与 `deviceCwdHonored` 拒绝并进 `cwdHonored` 同一条理由)。
|
|
70
|
+
*/
|
|
71
|
+
export declare function toolsRunOnThisHost(config: {
|
|
72
|
+
remoteExec?: {
|
|
73
|
+
provider?: string;
|
|
74
|
+
};
|
|
75
|
+
}): boolean;
|
|
55
76
|
/**
|
|
56
77
|
* device lane 的**平行**判别式(device-executor-lane-v2 §4.3.3,车A-4)——「员工在自己设备的项目目录
|
|
57
78
|
* 里干活」是这条车道的存在理由,所以 caller cwd 必须被尊重。
|
package/dist/task-cwd.js
CHANGED
|
@@ -20,6 +20,9 @@ export function dirCoversPath(dir, target) {
|
|
|
20
20
|
export function cwdHonored(config) {
|
|
21
21
|
return config.remoteExec?.provider === "host" && config.requirePrincipal !== true;
|
|
22
22
|
}
|
|
23
|
+
export function toolsRunOnThisHost(config) {
|
|
24
|
+
return config.remoteExec === undefined || config.remoteExec.provider === undefined || config.remoteExec.provider === "host";
|
|
25
|
+
}
|
|
23
26
|
export function deviceCwdHonored(config) {
|
|
24
27
|
return config.remoteExec?.provider === "device";
|
|
25
28
|
}
|
|
@@ -59,7 +59,7 @@ type _GuardMailboxReverse = AssertAllKeysHandled<Exclude<MailboxProjected, keyof
|
|
|
59
59
|
type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
|
|
60
60
|
type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
|
|
61
61
|
type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
|
|
62
|
-
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp";
|
|
62
|
+
type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "writeProtection";
|
|
63
63
|
type WiringSectionExcluded = "governance" | "configFingerprint";
|
|
64
64
|
type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
|
|
65
65
|
type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
|
|
@@ -100,6 +100,9 @@ type _GuardWiringMcpReverse = AssertAllKeysHandled<Exclude<WiringMcpHandled, key
|
|
|
100
100
|
type WiringRulesHandled = "storeWired" | "syncWired" | "orgGoverned";
|
|
101
101
|
type _GuardWiringRules = AssertAllKeysHandled<Exclude<keyof WiringSection<"permissionRules">, WiringRulesHandled>>;
|
|
102
102
|
type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled, keyof WiringSection<"permissionRules">>>;
|
|
103
|
+
type WiringWriteProtectionHandled = "targetView";
|
|
104
|
+
type _GuardWiringWriteProtection = AssertAllKeysHandled<Exclude<keyof WiringSection<"writeProtection">, WiringWriteProtectionHandled>>;
|
|
105
|
+
type _GuardWiringWriteProtectionReverse = AssertAllKeysHandled<Exclude<WiringWriteProtectionHandled, keyof WiringSection<"writeProtection">>>;
|
|
103
106
|
type WiringModelGateHandled = "class" | "removed" | "restore";
|
|
104
107
|
type _GuardWiringModelGate = AssertAllKeysHandled<Exclude<keyof WiringSection<"modelGate">, WiringModelGateHandled>>;
|
|
105
108
|
type _GuardWiringModelGateReverse = AssertAllKeysHandled<Exclude<WiringModelGateHandled, keyof WiringSection<"modelGate">>>;
|
|
@@ -180,6 +183,8 @@ export type CoreKeysetGuards = [
|
|
|
180
183
|
_GuardMailboxAppend,
|
|
181
184
|
_GuardMailboxAppendReverse,
|
|
182
185
|
_GuardWiringSections,
|
|
186
|
+
_GuardWiringWriteProtection,
|
|
187
|
+
_GuardWiringWriteProtectionReverse,
|
|
183
188
|
_GuardWiringSectionsReverse,
|
|
184
189
|
_GuardWiringLeg,
|
|
185
190
|
_GuardWiringLegReverse,
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -584,6 +584,26 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
584
584
|
* (「connection state is a fact about this leg's attempt, not about the assembly」)。
|
|
585
585
|
* ⚠️ 空数组 ≠ 缺席:core 顶注逐字「an empty array is "this leg declared no servers", absence is an older
|
|
586
586
|
* mint or an external derivation」⇒ 空数组照铸,别折成缺席。
|
|
587
|
+
* · `writeProtection`(`{ targetView: "spelling-only" | "target" }`;core 7.20.1 [ref] / clay [ref] 丙,
|
|
588
|
+
* S-361 提货件)—— **裁:租户可见,逐字透传**。它是 per-leg 的读面:「这条腿的写保护判官,拿到的是
|
|
589
|
+
* 哪一种目标读法」——`"target"` = 判官握着执行环境,拼法没命中时会在**解析后的真目标**上再判一次;
|
|
590
|
+
* `"spelling-only"` = 没有环境,拼法就是全部判决,只经别名/软链够到受保护行的写被清掉。
|
|
591
|
+
* ① **同主体同受众**:同一条腿、调用方自己的 run。而且这件事对该调用方**本就行为可观测** —— 在无手
|
|
592
|
+
* 车道上用一只声明了 `pathTarget` 写面的 MCP 工具对别名写一次就看得见(S-361 亲核可达)。剥掉它
|
|
593
|
+
* 并不收走任何能力,只收走那句诚实的标签,等于把 [ref] 刚刚消灭的**静默**在租户半场原样重建。
|
|
594
|
+
* ② **值的形**:core 的**两词闭集**,判据走 core 导出的 `WRITE_PROTECTION_TARGET_VIEWS` 常量(不抄
|
|
595
|
+
* 词表 —— 与 `delivered` 的 `MCP_DELIVERY_VERDICTS` 同一条);引擎派生、非用户内容 ⇒ 不脱敏、逐字。
|
|
596
|
+
* ③ 🔴 **最小披露仍然成立**:本键只答「判官有哪一种读法」,**从不**答「判官握着哪些路径」——
|
|
597
|
+
* 名表逐行内容(`rows` / `source` / `droppedDefaultRows`)照旧只在 operator 面
|
|
598
|
+
* (`GET /v1/diagnostics/wiring` 的 `writeProtection` 键,S-138),租户面照旧只有
|
|
599
|
+
* `/v1/capabilities.writeProtection.{armed, rows, replaced}` 三项计数。两者语义不同、同名不同物:
|
|
600
|
+
* 诊断面那一格是**本 server 装配的名表**(boot 产物),本段是**core 判官的自报读法**(per-leg)。
|
|
601
|
+
* ④ **不是 governance 侧信道**:它是「这条腿接没接手」的函数,与 lockedConfig/compliance/
|
|
602
|
+
* memoryAdmission/retention 四位无函数关系。⚠️ 但它**进** core 的 `configFingerprint`(core 顶注
|
|
603
|
+
* 逐字:which view the judge got is an ASSEMBLY fact)—— 指纹本就只发 operator 面,不受影响。
|
|
604
|
+
* ⚠️ **缺席 = 第三态**:core 的段是 **present-iff 编译出了判官**,`writeProtectedPaths: []` 的部署
|
|
605
|
+
* 整段缺席。缺席**绝不**折成 `"spelling-only"`(那是「判官在、但只认拼法」,完全不同的一件事),
|
|
606
|
+
* 也不铸 `writeProtection: {}`。词出闭集同样**不**折进缺席,走 `dropProjectedFace` 留痕(见那里)。
|
|
587
607
|
* 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
|
|
588
608
|
* (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来 —— ⚠️ **但那条钉是夹具驱动的**:它只看
|
|
589
609
|
* `deriveWiringManifest(EFFECTIVE_FACTS)` 真铸出来的段,facts 不带的可选段(147③ 的 `autoMode` 就是)它一格
|
package/dist/trace/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactErrorMessage, redactModelText } from "../observability/run-terminal-log.js";
|
|
2
|
-
import { MCP_DELIVERY_VERDICTS, screenGateOutcome, ToolRoster, ToolRosterDelta } from "@sema-agent/core";
|
|
2
|
+
import { MCP_DELIVERY_VERDICTS, screenGateOutcome, ToolRoster, ToolRosterDelta, WRITE_PROTECTION_TARGET_VIEWS } from "@sema-agent/core";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
4
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
5
|
import { redactSecrets, redactDeep } from "./redact.js";
|
|
@@ -481,6 +481,19 @@ export function wiringManifestEventData(ev) {
|
|
|
481
481
|
const lane = str(lspIn.lane);
|
|
482
482
|
return { mounted, ...(lane === undefined ? {} : { lane }) };
|
|
483
483
|
})();
|
|
484
|
+
const wpIn = sec(m.writeProtection);
|
|
485
|
+
const writeProtection = (() => {
|
|
486
|
+
if (m.writeProtection === undefined)
|
|
487
|
+
return undefined;
|
|
488
|
+
if (wpIn === undefined)
|
|
489
|
+
return dropProjectedFace("wiring_manifest.writeProtection is not an object");
|
|
490
|
+
const targetView = str(wpIn.targetView);
|
|
491
|
+
if (targetView === undefined)
|
|
492
|
+
return dropProjectedFace("wiring_manifest.writeProtection.targetView is not a non-empty string");
|
|
493
|
+
if (!WRITE_PROTECTION_TARGET_VIEWS.includes(targetView))
|
|
494
|
+
return dropProjectedFace("wiring_manifest.writeProtection.targetView is outside core WRITE_PROTECTION_TARGET_VIEWS");
|
|
495
|
+
return { targetView };
|
|
496
|
+
})();
|
|
484
497
|
const mcpIn = m.mcp;
|
|
485
498
|
const mcp = (() => {
|
|
486
499
|
if (!Array.isArray(mcpIn))
|
|
@@ -523,6 +536,7 @@ export function wiringManifestEventData(ev) {
|
|
|
523
536
|
...(mcp !== undefined ? { mcp } : {}),
|
|
524
537
|
...(hooks !== undefined ? { hooks } : {}),
|
|
525
538
|
...(lsp !== undefined ? { lsp } : {}),
|
|
539
|
+
...(writeProtection !== undefined ? { writeProtection } : {}),
|
|
526
540
|
...identityFields(ev),
|
|
527
541
|
};
|
|
528
542
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.80.
|
|
3
|
+
"version": "7.80.2",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "7.20.
|
|
57
|
+
"@sema-agent/core": "7.20.1",
|
|
58
58
|
"@sema-agent/settings-schema": "2.1.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|