@sema-agent/server 7.114.0 → 7.116.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/MIGRATION.md +237 -2
- package/USAGE.md +39 -3
- package/deploy/sema-up/cluster-up.sh +13 -1
- package/deploy/sema-up/kube-up.sh +13 -1
- package/deploy/sema-up/preflight.sh +13 -1
- package/deploy/sema-up/sema-up.sh +13 -1
- package/deploy/sema-up/smoke.sh +13 -1
- package/dist/adoption/wire.d.ts +0 -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 +25 -24
- package/dist/approval-card.js +14 -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 +7 -3
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/exit-boundary.d.ts +3 -2
- package/dist/boot/exit-boundary.js +3 -0
- package/dist/boot/resolve-spec.js +5 -1
- package/dist/boot/shutdown.d.ts +19 -0
- package/dist/boot/shutdown.js +12 -1
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- 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-catalog.js +2 -1
- package/dist/config-center/apply-effective.js +11 -11
- package/dist/config-provider.d.ts +16 -0
- package/dist/config-provider.js +11 -2
- package/dist/config-types.d.ts +20 -1
- package/dist/config.d.ts +55 -29
- package/dist/config.js +48 -25
- package/dist/deployment-governance.d.ts +15 -4
- package/dist/deployment-governance.js +20 -2
- package/dist/fleet/fleet-bus.d.ts +1 -22
- package/dist/hooks/hook-runner.d.ts +29 -3
- package/dist/hooks/hook-runner.js +30 -17
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +29 -7
- package/dist/http/approval-sweeps.js +20 -0
- package/dist/http/checkpoint-ctx.d.ts +68 -0
- package/dist/http/checkpoint-ctx.js +36 -0
- package/dist/http/durable-run-leg.js +2 -1
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +19 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-ctx.d.ts +7 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.d.ts +1 -3
- package/dist/http/routes/a2a-serve.js +14 -23
- 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.d.ts +0 -2
- package/dist/http/routes/admin-memory-optout.js +20 -7
- 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 +63 -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.d.ts +5 -0
- package/dist/http/routes/diagnostics.js +8 -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 +40 -25
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +5 -3
- 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/index.d.ts +1 -1
- package/dist/leader/wire.js +2 -1
- package/dist/local-data-root.d.ts +1 -1
- package/dist/mcp-content-origin.d.ts +0 -8
- package/dist/model-entry-refusal.d.ts +10 -2
- package/dist/model-entry-refusal.js +19 -3
- 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 +10 -9
- 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/permission-rule-list.d.ts +81 -0
- package/dist/permission-rule-list.js +17 -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 +52 -6
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/runtime-governance.d.ts +21 -1
- package/dist/runtime-governance.js +6 -1
- package/dist/security.js +2 -2
- package/dist/session-policy-wire.d.ts +40 -59
- 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 +17 -6
- package/dist/task-settings.js +16 -30
- package/dist/tool-approval.d.ts +32 -4
- package/dist/tool-approval.js +31 -8
- package/dist/trace/core-keyset-guard.d.ts +13 -8
- package/dist/trace/project.js +9 -0
- 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 +6 -4
- package/scripts/drain-check-local.mjs +528 -0
- package/scripts/drain-check.mysql.sql +67 -0
- package/scripts/drain-check.pg.sql +71 -0
- package/scripts/retired-mode-seed-drain-local.mjs +0 -317
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 {
|
|
10
|
-
import { deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
|
|
8
|
+
import * as facts from "../capability-facts.js";
|
|
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) {
|
|
@@ -202,7 +212,8 @@ export function createAdmission(ctx) {
|
|
|
202
212
|
}
|
|
203
213
|
const deadMatcher = deadToolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
|
|
204
214
|
if (deadMatcher !== undefined) {
|
|
205
|
-
|
|
215
|
+
const refusal = buildDeadToolMatcherRefusal(deadMatcher);
|
|
216
|
+
sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
|
|
206
217
|
return null;
|
|
207
218
|
}
|
|
208
219
|
}
|
|
@@ -360,10 +371,10 @@ export function createAdmission(ctx) {
|
|
|
360
371
|
return null;
|
|
361
372
|
}
|
|
362
373
|
{
|
|
363
|
-
const
|
|
374
|
+
const admission = facts.selfOrchestrationAdmission(deps);
|
|
364
375
|
const wantsSelfOrchestration = body.selfOrchestration === true
|
|
365
376
|
|| parseTaskSettings(body.settings).settings?.ultracode === true;
|
|
366
|
-
if (
|
|
377
|
+
if (!admission.available && wantsSelfOrchestration) {
|
|
367
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\"");
|
|
368
379
|
return null;
|
|
369
380
|
}
|
|
@@ -420,6 +431,10 @@ export function createAdmission(ctx) {
|
|
|
420
431
|
sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
|
|
421
432
|
return null;
|
|
422
433
|
}
|
|
434
|
+
if (l.brainCallGuardrailMs !== undefined) {
|
|
435
|
+
sendError(res, 400, "request.field_invalid", "limits.brainCallGuardrailMs is not caller-settable here — the engine's default wall on a single brain call applies (one hour); remove it");
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
423
438
|
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
424
439
|
const v = l[k];
|
|
425
440
|
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
@@ -473,6 +488,13 @@ export function createAdmission(ctx) {
|
|
|
473
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");
|
|
474
489
|
return null;
|
|
475
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
|
+
}
|
|
476
498
|
for (const key of ["excludeTools", "deferTools"]) {
|
|
477
499
|
const v = body[key];
|
|
478
500
|
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
@@ -7,6 +7,8 @@ import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
9
|
import { readinessOf, billableRefusalOf } from "./readiness.js";
|
|
10
|
+
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
11
|
+
import { allowListReadingUnstated } from "./checkpoint-ctx.js";
|
|
10
12
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
11
13
|
function blockedScalars(body) {
|
|
12
14
|
const out = {};
|
|
@@ -64,17 +66,35 @@ export function createApprovalSweeps(ctx) {
|
|
|
64
66
|
return false;
|
|
65
67
|
}
|
|
66
68
|
};
|
|
69
|
+
const allowListUnservable = async (sessionId) => {
|
|
70
|
+
const read = await readResumeCtx(cs.getCtx(sessionId));
|
|
71
|
+
if (read.kind === "corrupt")
|
|
72
|
+
return false;
|
|
73
|
+
return read.kind === "ok" && allowListReadingUnstated(read.ctx);
|
|
74
|
+
};
|
|
67
75
|
let parkedSkipped = 0;
|
|
76
|
+
const allowListSkipped = [];
|
|
68
77
|
for (const { sessionId } of expired) {
|
|
69
78
|
if (await isParkedOwned(sessionId)) {
|
|
70
79
|
parkedSkipped += 1;
|
|
71
80
|
continue;
|
|
72
81
|
}
|
|
82
|
+
if (await allowListUnservable(sessionId)) {
|
|
83
|
+
allowListSkipped.push(sessionId);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
73
86
|
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
|
|
74
87
|
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, ...blockedScalars(r.body) }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
|
|
75
88
|
}
|
|
76
89
|
if (parkedSkipped > 0)
|
|
77
90
|
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
91
|
+
if (allowListSkipped.length > 0) {
|
|
92
|
+
deps.logger?.warn?.("deny_sweep_allow_list_reading_skipped", {
|
|
93
|
+
count: allowListSkipped.length,
|
|
94
|
+
sessionIds: allowListSkipped,
|
|
95
|
+
note: "these expired approvals belong to tasks suspended by a server version that read settings.permissions.allow as a whitelist; this version does not replay them, so the SLA deny cannot be delivered. They stay suspended (session lock held) until the reaper collects them at their terminal time — cancel them (POST /v1/runs/:id/cancel) to release the sessions now",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
78
98
|
}
|
|
79
99
|
async function redeemParkedAsk(req) {
|
|
80
100
|
const cs = deps.checkpointStore;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 续跑重建输入(`checkpoint_ctx` 的行体,{@link CheckpointCtx})的**写形**,以及持久化体里 `settings.permissions.allow` 的**读法记号** —— S-1034(7.115.0)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病
|
|
5
|
+
* 7.115.0 把 `settings.permissions.allow` 从「名单」(名单外的工具一律拒)改成「allow 规则」(命中 = 带理由的放行,未命中 = 不表态、照常判)。
|
|
6
|
+
* 续跑家族重建 spec 时把**持久化的提交体**原样重放进 `resolveSpec`(`resume-legs.ts` 的 `resolveResumeSpec`)——
|
|
7
|
+
* 一条在 ≤7.114.0 上停驻、体里带非空 `allow` 的行,升级后续跑就被按新读法读:名单外的工具不再被拒。持久面上的静默语义翻转(放宽方向)。
|
|
8
|
+
* 坏形 `allow`(`Task` / `mcp__figma` / `Bash(npm:*)` …)的旧行则每次续跑都 409、SLA 拒扫腿逐拍重试直到收割 —— 一张本来放不开任何门的停驻被砖化。
|
|
9
|
+
*
|
|
10
|
+
* ## 规则(一条;持久数据的「响亮拒 + 一次性迁移」,零双读)
|
|
11
|
+
* 续跑重放持久化体之前问一句:**体里的 `allow` 在生效(非空数组)而本行没有「按表态读」的记号 ⇒ 拒**。
|
|
12
|
+
* 行留停驻、字节不动(拒发生在认领之前,run 行、卡、ctx 一个字节都不写);出路 = 作为新任务重提,这条任务若还停驻着先取消(`task_done` 唤醒停驻与 workflow 宿主那条腿没有可取消的 run)。
|
|
13
|
+
* **没有**「按旧名单语义继续读」的臂:那是第二种读法,正是本规则要消灭的东西。
|
|
14
|
+
* · 记号 = ≥7.115.0 写下的行在体里带非空 `allow` 时,ctx 上的 `permissionsAllowReading: "statement"`(闭集一词)。
|
|
15
|
+
* 写点 = 每一次**新的**持久化写({@link buildCheckpointCtx} 是它们的唯一构造口);续跑腿的写回传的是读进来的那只 ctx(或其展开),记号随行,不重新判。
|
|
16
|
+
* · 体里没有 `allow`(或空数组 —— 7.114.0 的名单判据 `acceptedRuleList` 对空数组同样读成「没有名单」)的行不写记号、不受本规则影响。
|
|
17
|
+
*
|
|
18
|
+
* ## 判据与 7.114.0 的关系(如实)
|
|
19
|
+
* 7.114.0 只在三桶整体合法时才读名单;判据这里只看「`allow` 是非空数组」,比 7.114.0 的「名单生效」宽一格(坏桶的行 7.114.0 也不读名单)——
|
|
20
|
+
* 方向 = 多拒,不会漏拒;而坏桶的体当年就被 fresh 受理门拒在门外,盘上本不该有。
|
|
21
|
+
*
|
|
22
|
+
* ## 何时删(一次性迁移,不是长期双读)
|
|
23
|
+
* 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件 = 所有 ≤7.114.0 停驻的行都已结束或被收割
|
|
24
|
+
* (停驻行的终局时刻 = 落盘起 `TERMINAL_BACKSTOP_MS`,见 `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删:
|
|
25
|
+
* 届时盘上不会再有没记号却带 `allow` 的行,而有记号的行读不读它都一样。
|
|
26
|
+
*/
|
|
27
|
+
import type { CheckpointCtx } from "./route-ctx.js";
|
|
28
|
+
import type { TaskRequestBody } from "./wire-types.js";
|
|
29
|
+
/** 读法记号的闭集(一词):这一行体里的 `settings.permissions.allow` 是按「allow 规则(表态)」写下、也该按它读的。 */
|
|
30
|
+
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["statement"];
|
|
31
|
+
export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
|
|
32
|
+
/**
|
|
33
|
+
* 体里的 `settings.permissions.allow` 是否**在生效**:非空数组(与 7.114.0 名单判据 `acceptedRuleList` 同一个「在」——空数组 = 没有)。
|
|
34
|
+
* 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
|
|
35
|
+
*/
|
|
36
|
+
export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 一次**新的**持久化写的 ctx(提交腿 / `POST /v1/runs` 族 / leader worker)—— 写记号的唯一口。
|
|
39
|
+
*
|
|
40
|
+
* 调用方是「本进程刚按表态读过这份体」的写点:fresh 提交(受理门按 7.115.0 读过 `allow`),或已经过了续跑拒臂的重放腿
|
|
41
|
+
* (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
|
|
42
|
+
* 续跑腿的**写回**不经本函数:它传读进来的 ctx(或其展开),记号随行;在写回口重新判会把一条没记号的旧行「洗」成新行。
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildCheckpointCtx(fields: {
|
|
45
|
+
body: TaskRequestBody;
|
|
46
|
+
memoryScope?: string | undefined;
|
|
47
|
+
unattendedLeg?: true;
|
|
48
|
+
}): CheckpointCtx;
|
|
49
|
+
/**
|
|
50
|
+
* 本行能否被按 7.115.0 的读法重放:体里 `allow` 在生效而行上没有记号 ⇒ **不能**(`true` = 拒)。
|
|
51
|
+
* 记号的值不在闭集里(手改 / 将来的写者)⇒ 同样拒:认不得的记号不是证据。
|
|
52
|
+
*/
|
|
53
|
+
export declare function allowListReadingUnstated(ctx: CheckpointCtx): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending;**不是**「原样重试会成功」)。
|
|
56
|
+
* 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子说原因(这条任务停在 `allow` 还是名单的版本上)与出路(作为新任务重提,仍停驻的先取消;要「只许这些工具」用会话规则 `toolAllow` / 每请求 `excludeTools`)。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 形是给冻结门看的(`api-error-text-freeze` 的 resume 家族收集器):`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
|
|
59
|
+
*/
|
|
60
|
+
export declare function allowListReadingRefusal(): {
|
|
61
|
+
status: number;
|
|
62
|
+
body: {
|
|
63
|
+
error: string;
|
|
64
|
+
errorCode: string;
|
|
65
|
+
retriable: true;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=checkpoint-ctx.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const PERMISSIONS_ALLOW_READINGS = ["statement"];
|
|
2
|
+
export function bodyCarriesAllowList(body) {
|
|
3
|
+
const settings = body?.settings;
|
|
4
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings))
|
|
5
|
+
return false;
|
|
6
|
+
const permissions = settings["permissions"];
|
|
7
|
+
if (permissions === null || typeof permissions !== "object" || Array.isArray(permissions))
|
|
8
|
+
return false;
|
|
9
|
+
const allow = permissions["allow"];
|
|
10
|
+
return Array.isArray(allow) && allow.length > 0;
|
|
11
|
+
}
|
|
12
|
+
export function buildCheckpointCtx(fields) {
|
|
13
|
+
return {
|
|
14
|
+
body: fields.body,
|
|
15
|
+
...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
|
|
16
|
+
...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
17
|
+
...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "statement" } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function allowListReadingUnstated(ctx) {
|
|
21
|
+
if (!bodyCarriesAllowList(ctx.body))
|
|
22
|
+
return false;
|
|
23
|
+
const reading = ctx.permissionsAllowReading;
|
|
24
|
+
return !(typeof reading === "string" && PERMISSIONS_ALLOW_READINGS.includes(reading));
|
|
25
|
+
}
|
|
26
|
+
export function allowListReadingRefusal() {
|
|
27
|
+
return {
|
|
28
|
+
status: 409,
|
|
29
|
+
body: {
|
|
30
|
+
error: "this task was suspended by an earlier server version, which read settings.permissions.allow as a whitelist (every tool not on the list was refused); this version reads that list as a statement that the named tools are allowed, not as a whitelist, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
|
|
31
|
+
errorCode: "checkpoint.unsupported_version",
|
|
32
|
+
retriable: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=checkpoint-ctx.js.map
|
|
@@ -8,6 +8,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
|
|
|
8
8
|
import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
9
9
|
import { clearTurnActivity, readTurnActivityMs } from "../turn-activity.js";
|
|
10
10
|
import { recordResourceWindow } from "../resource-window.js";
|
|
11
|
+
import { buildCheckpointCtx } from "./checkpoint-ctx.js";
|
|
11
12
|
export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
12
13
|
const { deps } = ctx;
|
|
13
14
|
const { inflightRuns, preemptableRuns, steerableRuns } = ctx.registry;
|
|
@@ -31,7 +32,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
|
|
|
31
32
|
return { status: placementReject.status, body: devicePlacementErrorBody(placementReject) };
|
|
32
33
|
commitSessionInheritEnv(deps, args.prepared.spec.sessionId, args.prepared.body);
|
|
33
34
|
if (deps.checkpointStore) {
|
|
34
|
-
await deps.checkpointStore.putCtx(sessionId, { body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope });
|
|
35
|
+
await deps.checkpointStore.putCtx(sessionId, buildCheckpointCtx({ body: args.prepared.body, memoryScope: args.prepared.auth?.memoryScope }));
|
|
35
36
|
}
|
|
36
37
|
const anchorOwner = args.prepared.auth?.principal ?? null;
|
|
37
38
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
@@ -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
|
@@ -42,6 +42,7 @@ import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js
|
|
|
42
42
|
import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
43
43
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
44
44
|
import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
|
|
45
|
+
import { allowListReadingRefusal, allowListReadingUnstated } from "./checkpoint-ctx.js";
|
|
45
46
|
import { httpErrorCode } from "./send.js";
|
|
46
47
|
function isCheckpointReopenedFailure(r) {
|
|
47
48
|
const code = failureCodeOf(r.terminal);
|
|
@@ -65,6 +66,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
65
66
|
read_face_damaged: false,
|
|
66
67
|
read_face_forged: false,
|
|
67
68
|
write_face_damaged: false,
|
|
69
|
+
skill_grants_damaged: false,
|
|
68
70
|
constraint_chain_missing: false,
|
|
69
71
|
};
|
|
70
72
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
@@ -120,13 +122,21 @@ export function checkpointErrorDisposition(e) {
|
|
|
120
122
|
const CHECKPOINT_ERROR_FIELD_ROLE = {
|
|
121
123
|
boundCallId: "binding",
|
|
122
124
|
boundInputHash: "binding",
|
|
125
|
+
boundGrantDigest: "regrant",
|
|
123
126
|
answer: "answer",
|
|
124
127
|
hostDecision: "outcome_cell",
|
|
125
128
|
approver: "outcome_cell",
|
|
126
129
|
taskConfig: "config",
|
|
127
130
|
inheritedGate: "config",
|
|
128
131
|
};
|
|
129
|
-
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]]));
|
|
130
140
|
function notResumableAnswer() {
|
|
131
141
|
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
132
142
|
}
|
|
@@ -398,7 +408,13 @@ export function createResumeLegs(ctx) {
|
|
|
398
408
|
});
|
|
399
409
|
return { spec, engineNotices };
|
|
400
410
|
};
|
|
411
|
+
const refuseAllowListReading = (sessionId) => {
|
|
412
|
+
deps.logger?.warn?.("resume_refused_allow_list_reading", { sessionId, note: "this session's stored request carries settings.permissions.allow written by a server that read it as a whitelist; it is not replayed under the current reading — the task stays suspended until someone cancels it" });
|
|
413
|
+
return allowListReadingRefusal();
|
|
414
|
+
};
|
|
401
415
|
const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
|
|
416
|
+
if (allowListReadingUnstated(ctx))
|
|
417
|
+
return { ok: false, answer: refuseAllowListReading(auth.sessionId) };
|
|
402
418
|
try {
|
|
403
419
|
const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth);
|
|
404
420
|
return { ok: true, spec, engineNotices };
|
|
@@ -1332,6 +1348,8 @@ export function createResumeLegs(ctx) {
|
|
|
1332
1348
|
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1333
1349
|
if (ctxRead.kind === "absent")
|
|
1334
1350
|
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1351
|
+
if (allowListReadingUnstated(ctxRead.ctx))
|
|
1352
|
+
return refuse(refuseAllowListReading(sessionId));
|
|
1335
1353
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
1336
1354
|
}
|
|
1337
1355
|
async function drivePlanReview(prepared, decision, editedPlan, reason, req, acceptEarly, permissionModeAfter) {
|
|
@@ -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
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -161,6 +161,13 @@ export interface CheckpointCtx {
|
|
|
161
161
|
* 传原 ctx / 其展开 ⇒ 标随行。缺席 = 不是无人值守腿(HTTP 提交腿;7.106.0 前落盘的行 —— 那时这些腿不铸模式,checkpoint 里也没有)。
|
|
162
162
|
*/
|
|
163
163
|
unattendedLeg?: true;
|
|
164
|
+
/**
|
|
165
|
+
* 🔴 S-1034(7.115.0)—— 体里 `settings.permissions.allow` 的**读法记号**:在场 = 这一行是 ≥7.115.0 写下的、体里带非空 `allow`,
|
|
166
|
+
* 它按「allow 规则(表态)」写、也按它读。**服务端写、不来自请求体**(与 {@link unattendedLeg} 同为 `body` 的兄弟键)。
|
|
167
|
+
* 缺席而体里带非空 `allow` = ≤7.114.0 停驻的行(那时 `allow` 是名单)⇒ 续跑腿响亮拒、行字节不动(规则与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注)。
|
|
168
|
+
* 写点只有 `buildCheckpointCtx`(同模块;新的持久化写);续跑腿的写回传原 ctx / 其展开 ⇒ 记号随行。体里没有 `allow` 的行不写它。
|
|
169
|
+
*/
|
|
170
|
+
permissionsAllowReading?: import("./checkpoint-ctx.js").PermissionsAllowReading;
|
|
164
171
|
}
|
|
165
172
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
166
173
|
export interface DriveResumeArgs {
|
|
@@ -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" }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import {
|
|
2
|
+
import type { A2ATaskState } from "@sema-agent/core";
|
|
3
3
|
import type { A2aServeConfig } from "../../config-types.js";
|
|
4
4
|
import { type RunRecord } from "../../plugins/store-contracts.js";
|
|
5
5
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
@@ -69,8 +69,6 @@ export declare function a2aStateForRun(run: Pick<RunRecord, "status" | "errorCod
|
|
|
69
69
|
* `capabilities.a2aServe` 的子串,好让 `route-shape-roster` 层E 如实认出它们。
|
|
70
70
|
*/
|
|
71
71
|
export declare function handleA2a(req: IncomingMessage, res: ServerResponse, match: RouteMatch<A2aRpcRouteId>, ctx: RouteCtx): Promise<void>;
|
|
72
|
-
/** 九词闭集的对外可见锚(测试用它证明我们只发闭集词;也让「加一个自造词」在这里就被看见)。 */
|
|
73
|
-
export declare const A2A_SERVE_TASK_STATES: readonly A2ATaskState[];
|
|
74
72
|
export declare const A2A_SERVE_ROUTES: readonly [{
|
|
75
73
|
readonly id: "a2a-agent-card";
|
|
76
74
|
readonly path: "/.well-known/agent-card.json";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { persistedPlaneOf } from "../../terminal.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
4
|
-
import { A2A_TASK_STATES } from "@sema-agent/core";
|
|
5
4
|
import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
|
|
6
5
|
import { HttpError } from "../../security.js";
|
|
7
6
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
@@ -9,6 +8,7 @@ import { sendJson, sendError } from "../send.js";
|
|
|
9
8
|
import { readinessOf, billableRefusalOf, sendBillableRefusal } from "../readiness.js";
|
|
10
9
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
11
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
|
+
import * as facts from "../../capability-facts.js";
|
|
12
12
|
import { createDurableRun } from "./runs.js";
|
|
13
13
|
export const A2A_PROTOCOL_VERSION = "1.0";
|
|
14
14
|
export const A2A_VERSION_HEADER = "a2a-version";
|
|
@@ -26,13 +26,13 @@ export async function handleA2aServe(req, res, match, ctx) {
|
|
|
26
26
|
const { deps } = ctx;
|
|
27
27
|
switch (match.decl.id) {
|
|
28
28
|
case "a2a-agent-card": {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const peer = facts.a2aServe(deps);
|
|
30
|
+
if (!peer.available) {
|
|
31
31
|
sendError(res, 404, "not_found.route", "not found");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
|
|
35
|
-
sendJson(res, 200, buildAgentCard(serve));
|
|
35
|
+
sendJson(res, 200, buildAgentCard(peer.serve));
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
default:
|
|
@@ -45,10 +45,8 @@ const JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
|
45
45
|
const JSONRPC_INVALID_PARAMS = -32602;
|
|
46
46
|
const JSONRPC_INTERNAL_ERROR = -32603;
|
|
47
47
|
const A2A_TASK_NOT_FOUND = -32001;
|
|
48
|
-
const A2A_UNSUPPORTED_OPERATION = -32004;
|
|
49
48
|
const A2A_CONTENT_TYPE_NOT_SUPPORTED = -32005;
|
|
50
49
|
export const A2A_TASK_NOT_FOUND_MESSAGE = "task not found";
|
|
51
|
-
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)";
|
|
52
50
|
const A2A_STATE_BY_RUN_STATUS = {
|
|
53
51
|
running: "working",
|
|
54
52
|
completed: "completed",
|
|
@@ -189,11 +187,8 @@ function readBlocking(params) {
|
|
|
189
187
|
function isStaleRun(run, runStaleSec) {
|
|
190
188
|
return run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > runStaleSec * 1000;
|
|
191
189
|
}
|
|
192
|
-
async function waitForRun(ctx, res, taskId, budgetMs) {
|
|
190
|
+
async function waitForRun(ctx, store, res, taskId, budgetMs) {
|
|
193
191
|
const { deps } = ctx;
|
|
194
|
-
const store = deps.runStore;
|
|
195
|
-
if (!store)
|
|
196
|
-
return null;
|
|
197
192
|
const deadline = Date.now() + budgetMs;
|
|
198
193
|
let last = null;
|
|
199
194
|
for (;;) {
|
|
@@ -220,7 +215,7 @@ async function waitForRun(ctx, res, taskId, budgetMs) {
|
|
|
220
215
|
});
|
|
221
216
|
}
|
|
222
217
|
}
|
|
223
|
-
async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
218
|
+
async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
224
219
|
const { deps } = ctx;
|
|
225
220
|
const { rateLimited, quotaExceeded } = ctx.helpers;
|
|
226
221
|
const message = asRecord(params.message, "params.message");
|
|
@@ -233,8 +228,6 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
233
228
|
const skillId = skill?.id;
|
|
234
229
|
const scenario = skill?.scenario;
|
|
235
230
|
const blocking = readBlocking(params);
|
|
236
|
-
if (!deps.runStore)
|
|
237
|
-
throw new RpcError(A2A_UNSUPPORTED_OPERATION, A2A_NO_RUN_STORE_MESSAGE);
|
|
238
231
|
const idemScope = `${ctx.req.source ?? ""}\x1f${gatedPrincipal(req, deps.config) ?? ""}`;
|
|
239
232
|
const fingerprint = createHash("sha256")
|
|
240
233
|
.update(messageId).update("\x1f")
|
|
@@ -249,7 +242,7 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
249
242
|
...(contextId !== undefined ? { sessionId: contextId } : {}),
|
|
250
243
|
...(scenario !== undefined ? { scenario } : {}),
|
|
251
244
|
};
|
|
252
|
-
const replay = await
|
|
245
|
+
const replay = await runStore.getRun(body.taskId).catch(() => undefined);
|
|
253
246
|
if (replay && (replay.owner === null || replay.owner === gatedPrincipal(req, deps.config))) {
|
|
254
247
|
return taskFromRun(replay, isStaleRun(replay, deps.config.runStaleSec));
|
|
255
248
|
}
|
|
@@ -281,7 +274,7 @@ async function handleMessageSend({ req, res, ctx, params, serve }) {
|
|
|
281
274
|
}
|
|
282
275
|
const taskId = receiptBody.taskId;
|
|
283
276
|
const sessionId = receiptBody.sessionId;
|
|
284
|
-
const run = await waitForRun(ctx, res, taskId, blocking ? serve.blockingWaitMs : 0);
|
|
277
|
+
const run = await waitForRun(ctx, runStore, res, taskId, blocking ? serve.blockingWaitMs : 0);
|
|
285
278
|
if (run)
|
|
286
279
|
return taskFromRun(run, isStaleRun(run, deps.config.runStaleSec));
|
|
287
280
|
return {
|
|
@@ -299,12 +292,10 @@ function durableIdempotencyTaskId(scope, messageId) {
|
|
|
299
292
|
const hex = b.toString("hex");
|
|
300
293
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
301
294
|
}
|
|
302
|
-
async function handleTasksGet({ req, ctx, params }) {
|
|
295
|
+
async function handleTasksGet({ req, ctx, params, runStore }) {
|
|
303
296
|
const { deps } = ctx;
|
|
304
297
|
const id = asNonEmptyString(params.id, "params.id", MAX_TASK_ID_CHARS);
|
|
305
|
-
|
|
306
|
-
throw new RpcError(A2A_UNSUPPORTED_OPERATION, A2A_NO_RUN_STORE_MESSAGE);
|
|
307
|
-
const run = await deps.runStore.getRun(id);
|
|
298
|
+
const run = await runStore.getRun(id);
|
|
308
299
|
if (!run)
|
|
309
300
|
throw new RpcError(A2A_TASK_NOT_FOUND, A2A_TASK_NOT_FOUND_MESSAGE);
|
|
310
301
|
if (run.owner !== null && gatedPrincipal(req, deps.config) !== run.owner)
|
|
@@ -323,11 +314,12 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
323
314
|
default:
|
|
324
315
|
return assertNever(match.decl.id);
|
|
325
316
|
}
|
|
326
|
-
const
|
|
327
|
-
if (!
|
|
317
|
+
const peer = facts.a2aServe(deps);
|
|
318
|
+
if (!peer.available) {
|
|
328
319
|
sendError(res, 404, "not_found.route", "not found");
|
|
329
320
|
return;
|
|
330
321
|
}
|
|
322
|
+
const { serve, runStore } = peer;
|
|
331
323
|
const requestedVersion = req.headers[A2A_VERSION_HEADER];
|
|
332
324
|
res.setHeader(A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION);
|
|
333
325
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
@@ -373,7 +365,7 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
373
365
|
}
|
|
374
366
|
deps.logger?.debug?.("a2a_serve_rpc", { method, ...(typeof requestedVersion === "string" ? { requestedVersion: requestedVersion.slice(0, 32) } : {}) });
|
|
375
367
|
try {
|
|
376
|
-
const result = await handler({ req, res, ctx, params: params, serve });
|
|
368
|
+
const result = await handler({ req, res, ctx, params: params, serve, runStore });
|
|
377
369
|
if (result === undefined) {
|
|
378
370
|
deps.metrics?.inc("a2a_serve_rpc_total", { method, outcome: "refused" });
|
|
379
371
|
return;
|
|
@@ -396,7 +388,6 @@ export async function handleA2a(req, res, match, ctx) {
|
|
|
396
388
|
function sendRpc(res, body) {
|
|
397
389
|
sendJson(res, 200, body);
|
|
398
390
|
}
|
|
399
|
-
export const A2A_SERVE_TASK_STATES = A2A_TASK_STATES;
|
|
400
391
|
export const A2A_SERVE_ROUTES = [
|
|
401
392
|
{ id: "a2a-agent-card", path: "/.well-known/agent-card.json", methods: ["GET"] },
|
|
402
393
|
];
|
|
@@ -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;
|