@sema-agent/server 4.1.1 → 4.1.3
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/approval.d.ts +3 -2
- package/dist/approval.js +0 -6
- package/dist/bake-runner/main.js +1 -5
- package/dist/boot/config-center.js +2 -1
- package/dist/boot/execution-env.d.ts +2 -2
- package/dist/boot/execution-env.js +17 -12
- package/dist/boot/resolve-spec.js +3 -3
- package/dist/bounded-session-map.d.ts +25 -0
- package/dist/bounded-session-map.js +37 -0
- package/dist/capabilities/center-plugins.d.ts +6 -1
- package/dist/capabilities/center-plugins.js +32 -9
- package/dist/capabilities/center-prompts.js +2 -1
- package/dist/capabilities/sandbox-file-send.d.ts +2 -2
- package/dist/capabilities/sandbox-file-send.js +6 -4
- package/dist/capabilities/select-environment-tool.d.ts +1 -2
- package/dist/capabilities/select-environment-tool.js +7 -11
- package/dist/capabilities/skills.d.ts +4 -0
- package/dist/capabilities/skills.js +5 -4
- package/dist/config-center/http-client.js +9 -0
- package/dist/config-center/skills-mcp.js +2 -1
- package/dist/digest-form.d.ts +15 -0
- package/dist/digest-form.js +15 -0
- package/dist/http/route-ctx.d.ts +3 -8
- package/dist/http/routes/approvals-assistant.js +3 -2
- package/dist/http/routes/runs.js +2 -2
- package/dist/http/routes/session-sync.js +11 -11
- package/dist/http/server.d.ts +2 -2
- package/dist/http/server.js +18 -40
- package/dist/http/verify-rounds.d.ts +24 -0
- package/dist/http/verify-rounds.js +18 -0
- package/dist/images/bake-validate.d.ts +5 -2
- package/dist/images/bake-validate.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/leader/grader-env-factory.js +1 -2
- package/dist/leader/merge.js +1 -1
- package/dist/leader/repair-oracle.js +1 -2
- package/dist/per-task-image.d.ts +1 -16
- package/dist/per-task-image.js +21 -11
- package/dist/plugins/approval-store-sql.js +2 -3
- package/dist/plugins/checkpoint-store-sql.js +2 -3
- package/dist/plugins/image-bake-store-sql.js +3 -3
- package/dist/plugins/outcome-ledger-sql.js +2 -3
- package/dist/plugins/remote-env-e2b.js +1 -5
- package/dist/plugins/remote-shell.d.ts +2 -1
- package/dist/plugins/remote-shell.js +2 -1
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/sql-driver.d.ts +17 -1
- package/dist/plugins/sql-driver.js +12 -0
- package/dist/prompts-domain-validate.d.ts +8 -0
- package/dist/prompts-domain-validate.js +25 -3
- package/dist/runs.d.ts +2 -4
- package/dist/security.d.ts +11 -0
- package/dist/security.js +11 -0
- package/dist/session-sync.d.ts +12 -1
- package/dist/session-sync.js +15 -3
- package/package.json +2 -2
package/dist/approval.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function hasOperatorGateIntent(config: {
|
|
|
35
35
|
* a **suspend** (persists a checkpoint, ends `status:"suspended"`) — no held connection, resumable on any
|
|
36
36
|
* replica. (Contrast {@link createOaApprovalPolicy}, which holds the call open by polling a TiDB row.)
|
|
37
37
|
*/
|
|
38
|
-
export
|
|
38
|
+
export interface DurableAskOptions {
|
|
39
39
|
requireApproval: string[];
|
|
40
40
|
deny?: string[];
|
|
41
41
|
autoBudget?: number;
|
|
@@ -46,7 +46,8 @@ export declare function createDurableAskPolicy(opts: {
|
|
|
46
46
|
exempt?: (canonicalToolName: string) => Promise<boolean>;
|
|
47
47
|
/** Audit hook — fired when an exemption short-circuits the ask (the `approval_exempted` log line). */
|
|
48
48
|
onExempted?: (canonicalToolName: string, rawToolName: string) => void;
|
|
49
|
-
}
|
|
49
|
+
}
|
|
50
|
+
export declare function createDurableAskPolicy(opts: DurableAskOptions): ToolPolicy;
|
|
50
51
|
export interface OaApprovalOptions {
|
|
51
52
|
store: ApprovalStore;
|
|
52
53
|
/** Tool names that require an operator decision before they run. */
|
package/dist/approval.js
CHANGED
|
@@ -30,12 +30,6 @@ export function hasOperatorGateIntent(config) {
|
|
|
30
30
|
config.approvalNeverAuto.length > 0 || // ← 漏了这格,见上面的由来
|
|
31
31
|
config.durableApproval);
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Durable approval policy (core 1.67 / design/45): returns three-state **`ask`** for gated tools instead of
|
|
35
|
-
* polling. With `TaskSpec.durableApproval` set + a `checkpointStore` on the Runner, core turns that `ask` into
|
|
36
|
-
* a **suspend** (persists a checkpoint, ends `status:"suspended"`) — no held connection, resumable on any
|
|
37
|
-
* replica. (Contrast {@link createOaApprovalPolicy}, which holds the call open by polling a TiDB row.)
|
|
38
|
-
*/
|
|
39
33
|
export function createDurableAskPolicy(opts) {
|
|
40
34
|
// 🔒 CANONICAL-space matching (mirrors core 1.202 `createAllowDenyPolicy` + the design/116 S8 discipline):
|
|
41
35
|
// configured names AND the live request name both normalize via `canonicalToolName`, so an operator rule written
|
package/dist/bake-runner/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { readFile } from "node:fs/promises";
|
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { createLogger } from "../observability/logger.js";
|
|
14
14
|
import { parseNumOrFailNonNegative } from "../config.js";
|
|
15
|
+
import { shellQuote as shellSafe } from "../plugins/remote-shell.js";
|
|
15
16
|
import { BakeRunner, } from "./runner.js";
|
|
16
17
|
const exec = promisify(execCb);
|
|
17
18
|
function loadEnv() {
|
|
@@ -224,11 +225,6 @@ function makeHostOps(env) {
|
|
|
224
225
|
},
|
|
225
226
|
};
|
|
226
227
|
}
|
|
227
|
-
/** Shell-quote a single argument for the `exec` string forms above (df/git/docker take server-fixed paths/refs, but
|
|
228
|
-
* a defensive quote keeps a surprising RECIPE_DIR/ref from breaking the command — argv to build.sh is a vector). */
|
|
229
|
-
function shellSafe(s) {
|
|
230
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
231
|
-
}
|
|
232
228
|
function makeClock() {
|
|
233
229
|
return {
|
|
234
230
|
now: () => Date.now(),
|
|
@@ -46,6 +46,7 @@ import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../con
|
|
|
46
46
|
import { createKeyResolver } from "../key-resolver.js";
|
|
47
47
|
import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
|
|
48
48
|
import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
|
|
49
|
+
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
49
50
|
export async function createConfigCenterRuntime(ctx) {
|
|
50
51
|
const { config, logger, metrics, localRoot } = ctx;
|
|
51
52
|
// Config-center: pull the effective config on boot and apply it OVER the env
|
|
@@ -324,7 +325,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
324
325
|
const cacheDir = defaultSkillCacheDir();
|
|
325
326
|
for (const m of enabledSkills) {
|
|
326
327
|
const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
|
|
327
|
-
if (
|
|
328
|
+
if (!SHA256_HEX_RE.test(hex))
|
|
328
329
|
return false;
|
|
329
330
|
try {
|
|
330
331
|
const text = await fsReadFile(join(cacheDir, hex), "utf8");
|
|
@@ -16,8 +16,8 @@ export declare function createExecutionEnv(ctx: ExecutionEnvCtx): {
|
|
|
16
16
|
perTaskImage: PerTaskImageRegistry;
|
|
17
17
|
sessionEnvSelection: SessionEnvironmentSelection;
|
|
18
18
|
perSessionCwd: Map<string, string>;
|
|
19
|
-
setSessionCwd: (sid: string,
|
|
20
|
-
setSessionShellEnv: (sid: string,
|
|
19
|
+
setSessionCwd: (sid: string, value: string) => void;
|
|
20
|
+
setSessionShellEnv: (sid: string, value: Record<string, string>) => void;
|
|
21
21
|
executionEnvFactory: import("@sema-agent/core").ExecutionEnvFactory | undefined;
|
|
22
22
|
worktreeReap: (() => Promise<void>) | undefined;
|
|
23
23
|
sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
|
|
@@ -34,6 +34,21 @@ import { materializeAttachmentsInto } from "../plugins/task-attachment-store.js"
|
|
|
34
34
|
import { reapOrphanWorktrees, withWorktreeIsolation } from "../plugins/worktree-isolation.js";
|
|
35
35
|
import { customPkgSourceFromEnv, derivePkgSourceEnv } from "../sandbox-pkg-source.js";
|
|
36
36
|
import { effectiveHostWorkspace } from "../task-cwd.js";
|
|
37
|
+
/**
|
|
38
|
+
* #97 R11: the perSessionCwd/perSessionShellEnv setters below share one idiom (delete-then-set so a
|
|
39
|
+
* re-set moves the tail to MRU, then oldest-first eviction on overflow). Local to this file — NOT the
|
|
40
|
+
* public {@link import("../bounded-session-map.js").BoundedSessionMap} seam, because both callers below
|
|
41
|
+
* still hand out the raw `Map` (its declared type threads through `resolve-spec.ts` as `Map<string, V>`,
|
|
42
|
+
* not a class instance) — this only DRYs the two identical closures, it changes no public shape.
|
|
43
|
+
*/
|
|
44
|
+
function boundedSessionSetter(map, maxEntries) {
|
|
45
|
+
return (sid, value) => {
|
|
46
|
+
map.delete(sid); // re-insert at the tail = most-recently-used
|
|
47
|
+
map.set(sid, value);
|
|
48
|
+
if (map.size > maxEntries)
|
|
49
|
+
map.delete(map.keys().next().value); // evict oldest
|
|
50
|
+
};
|
|
51
|
+
}
|
|
37
52
|
export function createExecutionEnv(ctx) {
|
|
38
53
|
const { config, logger, metrics, taskAttachmentStore } = ctx;
|
|
39
54
|
// design/48 v1b: deployment-level remote execution(部署级路由 + 懒汉连接). When
|
|
@@ -54,22 +69,12 @@ export function createExecutionEnv(ctx) {
|
|
|
54
69
|
// re-sends cwd on every request, so evicting a stale session is harmless (it re-registers on next use).
|
|
55
70
|
const MAX_CWD_SESSIONS = 4096;
|
|
56
71
|
const perSessionCwd = new Map();
|
|
57
|
-
const setSessionCwd = (
|
|
58
|
-
perSessionCwd.delete(sid); // re-insert at the tail = most-recently-used
|
|
59
|
-
perSessionCwd.set(sid, cwd);
|
|
60
|
-
if (perSessionCwd.size > MAX_CWD_SESSIONS)
|
|
61
|
-
perSessionCwd.delete(perSessionCwd.keys().next().value); // evict oldest
|
|
62
|
-
};
|
|
72
|
+
const setSessionCwd = boundedSessionSetter(perSessionCwd, MAX_CWD_SESSIONS);
|
|
63
73
|
// [R-survey / TOC shellEnv seam, core PLAN批注] per-session `settings.env` → the host lane's agent shell env
|
|
64
74
|
// (resolveSpec registers it gated by cwdHonored — single-user host lane only; the host factory merges it by
|
|
65
75
|
// ctx.sessionId). design/107 "env = capability axis". Same LRU bound + re-send-on-every-request semantics as cwd.
|
|
66
76
|
const perSessionShellEnv = new Map();
|
|
67
|
-
const setSessionShellEnv = (
|
|
68
|
-
perSessionShellEnv.delete(sid);
|
|
69
|
-
perSessionShellEnv.set(sid, env);
|
|
70
|
-
if (perSessionShellEnv.size > MAX_CWD_SESSIONS)
|
|
71
|
-
perSessionShellEnv.delete(perSessionShellEnv.keys().next().value);
|
|
72
|
-
};
|
|
77
|
+
const setSessionShellEnv = boundedSessionSetter(perSessionShellEnv, MAX_CWD_SESSIONS);
|
|
73
78
|
let executionEnvFactory;
|
|
74
79
|
// SVC-3 worktree isolation: the reaper (defined far below) reuses ONE long-lived git base env + repoRoot to
|
|
75
80
|
// `git worktree prune` crash-orphaned worktrees. Holders are populated when the wrapper is wired (host lane).
|
|
@@ -39,7 +39,7 @@ import { resourceSuspendOptIn } from "../resource-suspend.js";
|
|
|
39
39
|
import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../router/route-orchestration.js";
|
|
40
40
|
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
41
41
|
import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
|
|
42
|
-
import { HttpError, memorySpecForRequest } from "../security.js";
|
|
42
|
+
import { HttpError, memorySpecForRequest, encodeCheckpointScope } from "../security.js";
|
|
43
43
|
import { mcpForScenario } from "../config-center/facade.js";
|
|
44
44
|
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolNameListFromBody } from "../spec-fields.js";
|
|
45
45
|
import { cwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
|
|
@@ -747,7 +747,7 @@ export function createResolveSpec(ctx) {
|
|
|
747
747
|
? {
|
|
748
748
|
checkpointStore,
|
|
749
749
|
durableApproval: {
|
|
750
|
-
scope: auth?.principal
|
|
750
|
+
scope: encodeCheckpointScope(auth?.principal),
|
|
751
751
|
...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
|
|
752
752
|
},
|
|
753
753
|
// design/80 seam #2: opt this task into resource/preempt durable-suspend (design/74 third state)
|
|
@@ -760,7 +760,7 @@ export function createResolveSpec(ctx) {
|
|
|
760
760
|
ttlSec: config.resourceSuspendTtlSec,
|
|
761
761
|
isVerify: body.verify === true,
|
|
762
762
|
isCascade: body.cascade === true,
|
|
763
|
-
scope: auth?.principal
|
|
763
|
+
scope: encodeCheckpointScope(auth?.principal),
|
|
764
764
|
})),
|
|
765
765
|
onQuestion: QUESTION_AWAITS_RESUME,
|
|
766
766
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #97 R11 收编:两个既有 session-keyed registry({@link import("./per-task-image.js").PerTaskImageRegistry}
|
|
3
|
+
* 与 {@link import("./capabilities/select-environment-tool.js").SessionEnvironmentSelection})共享的同一形
|
|
4
|
+
* (delete-then-set + 容量闩逐最老 + get 吞 undefined)收编到一处——基准实现逐字取自
|
|
5
|
+
* `PerTaskImageRegistry`(唯一行为变化:泛型化 value 类型)。两消费类改为持有本类的组合薄壳,自己的
|
|
6
|
+
* 构造器/方法签名不变。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* KEYED BY sessionId, NON-removing `get`, bounded by oldest-first eviction on overflow.
|
|
10
|
+
*
|
|
11
|
+
* `set` re-inserts at the tail (Map iteration order = insertion order) via delete-then-set, so a re-set
|
|
12
|
+
* of an EXISTING key becomes the newest entry — the eviction below always removes the map's current
|
|
13
|
+
* first key, so a key that was just re-set is the LAST to be evicted, not the first.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BoundedSessionMap<V> {
|
|
16
|
+
private readonly maxEntries;
|
|
17
|
+
private readonly map;
|
|
18
|
+
constructor(maxEntries?: number);
|
|
19
|
+
/** A re-set (same key) overwrites and moves the entry to the newest position; on overflow the oldest
|
|
20
|
+
* insertion is evicted. */
|
|
21
|
+
set(sessionId: string, value: V): void;
|
|
22
|
+
/** `undefined` sessionId or an unregistered session both → `undefined` (non-removing read). */
|
|
23
|
+
get(sessionId: string | undefined): V | undefined;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=bounded-session-map.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #97 R11 收编:两个既有 session-keyed registry({@link import("./per-task-image.js").PerTaskImageRegistry}
|
|
3
|
+
* 与 {@link import("./capabilities/select-environment-tool.js").SessionEnvironmentSelection})共享的同一形
|
|
4
|
+
* (delete-then-set + 容量闩逐最老 + get 吞 undefined)收编到一处——基准实现逐字取自
|
|
5
|
+
* `PerTaskImageRegistry`(唯一行为变化:泛型化 value 类型)。两消费类改为持有本类的组合薄壳,自己的
|
|
6
|
+
* 构造器/方法签名不变。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* KEYED BY sessionId, NON-removing `get`, bounded by oldest-first eviction on overflow.
|
|
10
|
+
*
|
|
11
|
+
* `set` re-inserts at the tail (Map iteration order = insertion order) via delete-then-set, so a re-set
|
|
12
|
+
* of an EXISTING key becomes the newest entry — the eviction below always removes the map's current
|
|
13
|
+
* first key, so a key that was just re-set is the LAST to be evicted, not the first.
|
|
14
|
+
*/
|
|
15
|
+
export class BoundedSessionMap {
|
|
16
|
+
maxEntries;
|
|
17
|
+
map = new Map();
|
|
18
|
+
constructor(maxEntries = 4096) {
|
|
19
|
+
this.maxEntries = maxEntries;
|
|
20
|
+
}
|
|
21
|
+
/** A re-set (same key) overwrites and moves the entry to the newest position; on overflow the oldest
|
|
22
|
+
* insertion is evicted. */
|
|
23
|
+
set(sessionId, value) {
|
|
24
|
+
this.map.delete(sessionId);
|
|
25
|
+
this.map.set(sessionId, value);
|
|
26
|
+
if (this.map.size > this.maxEntries) {
|
|
27
|
+
const oldest = this.map.keys().next().value;
|
|
28
|
+
if (oldest !== undefined)
|
|
29
|
+
this.map.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** `undefined` sessionId or an unregistered session both → `undefined` (non-removing read). */
|
|
33
|
+
get(sessionId) {
|
|
34
|
+
return sessionId === undefined ? undefined : this.map.get(sessionId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=bounded-session-map.js.map
|
|
@@ -38,9 +38,14 @@ export declare function gitClonePlan(o: {
|
|
|
38
38
|
steps: string[][];
|
|
39
39
|
env: Record<string, string>;
|
|
40
40
|
};
|
|
41
|
+
/** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
|
|
42
|
+
* 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
|
|
43
|
+
export declare const PLUGIN_MANIFEST_MAX_BYTES: number;
|
|
41
44
|
/** 条款 5 的 manifest 半场:`.claude-plugin/plugin.json` 的 `skillsPath`(缺省 `skills/`;cli plugin
|
|
42
45
|
* 布局同族)。返回 ok:true+dir:undefined = 插件没有 skills 目录(可以只有 commands——那是 cli 壳的
|
|
43
|
-
* 半场,worker 装 0 个 skill 不算失败);ok:false =
|
|
46
|
+
* 半场,worker 装 0 个 skill 不算失败);ok:false = 路径逃逸/manifest 本体畸形,整条拒装。
|
|
47
|
+
* manifest 臂分立(V2-⑦,原 broad-catch 是 fail-open):**不存在**=可选面,缺省 skills/;**存在但**
|
|
48
|
+
* 本体逃逸根外/非普通文件/超界/读失败/JSON 坏 = ok:false 走既有 warning 面。 */
|
|
44
49
|
export declare function resolvePluginSkillsRoot(cloneRoot: string): {
|
|
45
50
|
ok: true;
|
|
46
51
|
dir?: string;
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { execFile } from "node:child_process";
|
|
24
24
|
import { promisify } from "node:util";
|
|
25
|
-
import { promises as fsp, realpathSync, existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, statSync } from "node:fs";
|
|
26
26
|
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
27
|
-
import { loadSkills } from "./skills.js";
|
|
27
|
+
import { assertConfined, loadSkills } from "./skills.js";
|
|
28
28
|
const execFileP = promisify(execFile);
|
|
29
29
|
/** 缺省 clone host 白名单(契约条款 1;国内镜像走本名单加 https 镜像域,不开 http 口)。 */
|
|
30
30
|
export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
|
|
@@ -83,22 +83,45 @@ export function gitClonePlan(o) {
|
|
|
83
83
|
env,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
/** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
|
|
87
|
+
* 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
|
|
88
|
+
export const PLUGIN_MANIFEST_MAX_BYTES = 64 * 1024;
|
|
86
89
|
/** 条款 5 的 manifest 半场:`.claude-plugin/plugin.json` 的 `skillsPath`(缺省 `skills/`;cli plugin
|
|
87
90
|
* 布局同族)。返回 ok:true+dir:undefined = 插件没有 skills 目录(可以只有 commands——那是 cli 壳的
|
|
88
|
-
* 半场,worker 装 0 个 skill 不算失败);ok:false =
|
|
91
|
+
* 半场,worker 装 0 个 skill 不算失败);ok:false = 路径逃逸/manifest 本体畸形,整条拒装。
|
|
92
|
+
* manifest 臂分立(V2-⑦,原 broad-catch 是 fail-open):**不存在**=可选面,缺省 skills/;**存在但**
|
|
93
|
+
* 本体逃逸根外/非普通文件/超界/读失败/JSON 坏 = ok:false 走既有 warning 面。 */
|
|
89
94
|
export function resolvePluginSkillsRoot(cloneRoot) {
|
|
90
95
|
let skillsPath = "skills";
|
|
96
|
+
const rootReal = realpathSync(cloneRoot);
|
|
97
|
+
const manifestPath = join(cloneRoot, ".claude-plugin", "plugin.json");
|
|
98
|
+
let manifestPresent = false;
|
|
91
99
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
skillsPath = manifest.skillsPath;
|
|
100
|
+
lstatSync(manifestPath); // 条目在场判定(lstat:悬空 symlink 也算在场——它该走拒绝臂,不是缺席臂)
|
|
101
|
+
manifestPresent = true;
|
|
95
102
|
}
|
|
96
103
|
catch {
|
|
97
|
-
/* 无 manifest
|
|
104
|
+
/* 无 manifest ⇒ 缺省 skills/(manifest 是可选面) */
|
|
105
|
+
}
|
|
106
|
+
if (manifestPresent) {
|
|
107
|
+
try {
|
|
108
|
+
assertConfined(manifestPath, rootReal, "plugin manifest");
|
|
109
|
+
const st = statSync(manifestPath);
|
|
110
|
+
if (!st.isFile())
|
|
111
|
+
return { ok: false, reason: `plugin manifest is not a regular file (${manifestPath})` };
|
|
112
|
+
if (st.size > PLUGIN_MANIFEST_MAX_BYTES) {
|
|
113
|
+
return { ok: false, reason: `plugin manifest exceeds ${PLUGIN_MANIFEST_MAX_BYTES} bytes (got ${st.size})` };
|
|
114
|
+
}
|
|
115
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
116
|
+
if (typeof manifest.skillsPath === "string" && manifest.skillsPath !== "")
|
|
117
|
+
skillsPath = manifest.skillsPath;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return { ok: false, reason: `plugin manifest rejected: ${e instanceof Error ? e.message : String(e)}` };
|
|
121
|
+
}
|
|
98
122
|
}
|
|
99
123
|
if (isAbsolute(skillsPath))
|
|
100
124
|
return { ok: false, reason: `skillsPath must be relative (got ${skillsPath})` };
|
|
101
|
-
const rootReal = realpathSync(cloneRoot);
|
|
102
125
|
const candidate = resolve(rootReal, skillsPath);
|
|
103
126
|
// 先做词法前缀判定(../ 逃逸在 realpath 之前就拒——目标可能不存在)
|
|
104
127
|
if (candidate !== rootReal && !candidate.startsWith(rootReal + sep)) {
|
|
@@ -180,7 +203,7 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
180
203
|
}
|
|
181
204
|
if (!rootCheck.dir)
|
|
182
205
|
continue; // 无 skills 目录:合法(commands-only 插件,cli 壳的半场)
|
|
183
|
-
const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir }); // 不可信 lane:逐条目约束在 clone root 内(根守卫只守了 skills 根,根下 symlink 可逃逸)
|
|
206
|
+
const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir }); // 不可信 lane:逐条目约束在 clone root 内(根守卫只守了 skills 根,根下 symlink 可逃逸)
|
|
184
207
|
for (const skill of loaded) {
|
|
185
208
|
if (taken.has(skill.spec.name)) {
|
|
186
209
|
// 收紧④:plugin 让位(方向与 center-wins 相反,理由见模块顶注)。
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { verifyPromptArtifact } from "@sema-agent/core";
|
|
23
23
|
import { CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
|
|
24
|
+
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
24
25
|
/** 判形半场(validateCenterPrompts/validatePromptsDomain/CORE_ENGINE_VERSION 及两 interface)已下沉到
|
|
25
26
|
* 中立叶子 `src/prompts-domain-validate.ts`(design/158 S6,lens2 §G:消 config-lkg → capabilities 的
|
|
26
27
|
* 唯一分层反向边)。此处 re-export 保兼容——既有消费者(main/run-local/测试)零改动。 */
|
|
@@ -39,7 +40,7 @@ export function withPromptArtifactBackfill(inner, fetchRaw, log) {
|
|
|
39
40
|
put: (e) => inner.put(e),
|
|
40
41
|
get: async (digest, opts) => {
|
|
41
42
|
const hex = digest.replace(/^sha256:/i, "").toLowerCase();
|
|
42
|
-
if (
|
|
43
|
+
if (!SHA256_HEX_RE.test(hex))
|
|
43
44
|
return undefined; // malformed request — never a network trip
|
|
44
45
|
const canonical = `sha256:${hex}`;
|
|
45
46
|
const local = await inner.get(canonical, opts);
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import type { ExecutionEnv, ExecutionEnvFactory } from "@sema-agent/core";
|
|
37
37
|
import type { IssuedFileLink, PreparedDirectUpload } from "../plugins/send-user-file.js";
|
|
38
|
+
import { shellQuote as shq } from "../plugins/remote-shell.js";
|
|
38
39
|
/** Leak backstop (codex review HIGH): core skips destroy on suspend/review paths and a crash may skip it
|
|
39
40
|
* entirely — the suspendVM wrap covers the common case, this bound covers the rest. Far above any real
|
|
40
41
|
* per-replica concurrency. 修6: at the cap the registry REFUSES the new registration instead of FIFO-evicting
|
|
@@ -91,8 +92,7 @@ export declare class TaskEnvRegistry {
|
|
|
91
92
|
* adb/local-docker remain a follow-on).
|
|
92
93
|
*/
|
|
93
94
|
export declare function sandboxSendLaneEnabled(provider: string | undefined, requirePrincipal: boolean): boolean;
|
|
94
|
-
|
|
95
|
-
export declare function shq(s: string): string;
|
|
95
|
+
export { shq };
|
|
96
96
|
/** Strip the capability URL (and any SigV4 signature) from text before it reaches an error/log — including
|
|
97
97
|
* the percent-encoded forms an adapter may echo (the k8s exec transport carries the command in a request
|
|
98
98
|
* URI, so an error can quote the URL re-encoded; codex review MED). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasDestroy } from "@sema-agent/core";
|
|
2
|
+
import { shellQuote as shq } from "../plugins/remote-shell.js";
|
|
2
3
|
/** Leak backstop (codex review HIGH): core skips destroy on suspend/review paths and a crash may skip it
|
|
3
4
|
* entirely — the suspendVM wrap covers the common case, this bound covers the rest. Far above any real
|
|
4
5
|
* per-replica concurrency. 修6: at the cap the registry REFUSES the new registration instead of FIFO-evicting
|
|
@@ -138,10 +139,11 @@ export function sandboxSendLaneEnabled(provider, requirePrincipal) {
|
|
|
138
139
|
return requirePrincipal !== true;
|
|
139
140
|
return false;
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// design/158 R10: the quoting logic itself now lives solely in remote-shell.ts's shellQuote (this module's
|
|
143
|
+
// former standalone copy was byte-identical; its JSDoc note was folded into that owner). `shq` stays a
|
|
144
|
+
// re-exported alias — not a plain call-through rename — because two test files import `shq` by name from
|
|
145
|
+
// this module.
|
|
146
|
+
export { shq };
|
|
145
147
|
/** Strip the capability URL (and any SigV4 signature) from text before it reaches an error/log — including
|
|
146
148
|
* the percent-encoded forms an adapter may echo (the k8s exec transport carries the command in a request
|
|
147
149
|
* URI, so an error can quote the URL re-encoded; codex review MED). */
|
|
@@ -13,8 +13,7 @@ export interface EnvironmentCatalog extends SandboxImageResolver {
|
|
|
13
13
|
* honest v1 boundary: a worker restart drops the selection (the model can re-select; nothing fails closed
|
|
14
14
|
* the wrong way, the session just falls back to the worker default image). */
|
|
15
15
|
export declare class SessionEnvironmentSelection {
|
|
16
|
-
private readonly
|
|
17
|
-
private readonly map;
|
|
16
|
+
private readonly bounded;
|
|
18
17
|
constructor(maxEntries?: number);
|
|
19
18
|
set(sessionId: string, profile: string): void;
|
|
20
19
|
get(sessionId: string | undefined): string | undefined;
|
|
@@ -21,28 +21,24 @@
|
|
|
21
21
|
* (tenant-neutral profile names only — the list enters model context).
|
|
22
22
|
*/
|
|
23
23
|
import { Type } from "typebox";
|
|
24
|
+
import { BoundedSessionMap } from "../bounded-session-map.js";
|
|
24
25
|
import { resolveSandboxImageRef } from "../per-task-image.js";
|
|
25
26
|
/** Session-keyed selected PROFILE (intent). Mirrors PerTaskImageRegistry's bounded-eviction shape, but holds
|
|
26
27
|
* the profile string so every subsequent task re-resolves fail-closed (re-admit discipline). In-memory —
|
|
27
28
|
* honest v1 boundary: a worker restart drops the selection (the model can re-select; nothing fails closed
|
|
28
29
|
* the wrong way, the session just falls back to the worker default image). */
|
|
29
30
|
export class SessionEnvironmentSelection {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// #97 R11: same BoundedSessionMap basis as PerTaskImageRegistry (composition, not inheritance) — this
|
|
32
|
+
// class's own ctor/method surface is unchanged.
|
|
33
|
+
bounded;
|
|
32
34
|
constructor(maxEntries = 4096) {
|
|
33
|
-
this.
|
|
35
|
+
this.bounded = new BoundedSessionMap(maxEntries);
|
|
34
36
|
}
|
|
35
37
|
set(sessionId, profile) {
|
|
36
|
-
this.
|
|
37
|
-
this.map.set(sessionId, profile);
|
|
38
|
-
if (this.map.size > this.maxEntries) {
|
|
39
|
-
const oldest = this.map.keys().next().value;
|
|
40
|
-
if (oldest !== undefined)
|
|
41
|
-
this.map.delete(oldest);
|
|
42
|
-
}
|
|
38
|
+
this.bounded.set(sessionId, profile);
|
|
43
39
|
}
|
|
44
40
|
get(sessionId) {
|
|
45
|
-
return
|
|
41
|
+
return this.bounded.get(sessionId);
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
const capList = (caps) => Object.entries(caps ?? {})
|
|
@@ -42,6 +42,10 @@ export interface LoadSkillsOptions {
|
|
|
42
42
|
/** 不可信 lane 的约束根(通常=clone root)。缺席=受控 lane,不做逐条目约束。 */
|
|
43
43
|
confineTo?: string;
|
|
44
44
|
}
|
|
45
|
+
/** `p` 的 realpath 必须落在 `rootReal` 之内(含自身),否则抛。`sep` 边界判定防 `/a/root-evil` 撞
|
|
46
|
+
* `/a/root` 前缀。目标不存在 ⇒ realpath 抛 ENOENT,同样是拒绝(悬空链接不该被读)。
|
|
47
|
+
* `label` = 错误文本里的对象名(center-plugins 用它约束 manifest 本体,报错不该指成 skill entry)。 */
|
|
48
|
+
export declare function assertConfined(p: string, rootReal: string, label?: string): void;
|
|
45
49
|
export declare function loadSkills(dir: string, opts?: LoadSkillsOptions): LoadedSkill[];
|
|
46
50
|
/** Skills applicable to a scenario: those tagged with it, plus untagged (global) ones. */
|
|
47
51
|
export declare function skillsForScenario(loaded: LoadedSkill[], scenario: string): SkillSpec[];
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { join, sep } from "node:path";
|
|
3
3
|
/** `p` 的 realpath 必须落在 `rootReal` 之内(含自身),否则抛。`sep` 边界判定防 `/a/root-evil` 撞
|
|
4
|
-
* `/a/root` 前缀。目标不存在 ⇒ realpath 抛 ENOENT,同样是拒绝(悬空链接不该被读)。
|
|
5
|
-
|
|
4
|
+
* `/a/root` 前缀。目标不存在 ⇒ realpath 抛 ENOENT,同样是拒绝(悬空链接不该被读)。
|
|
5
|
+
* `label` = 错误文本里的对象名(center-plugins 用它约束 manifest 本体,报错不该指成 skill entry)。 */
|
|
6
|
+
export function assertConfined(p, rootReal, label = "skill entry") {
|
|
6
7
|
let real;
|
|
7
8
|
try {
|
|
8
9
|
real = realpathSync(p);
|
|
9
10
|
}
|
|
10
11
|
catch (e) {
|
|
11
|
-
throw new Error(
|
|
12
|
+
throw new Error(`${label} ${p} is unreadable (dangling symlink or missing): ${e instanceof Error ? e.message : String(e)}`);
|
|
12
13
|
}
|
|
13
14
|
if (real !== rootReal && !real.startsWith(rootReal + sep)) {
|
|
14
|
-
throw new Error(
|
|
15
|
+
throw new Error(`${label} ${p} escapes the plugin root (resolves outside ${rootReal}) — untrusted checkouts may not link to host files`);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
export function loadSkills(dir, opts) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { readEffectiveWire } from "@sema-agent/registry-core";
|
|
9
|
+
import { centerPromptsFromEffective } from "../prompts-domain-validate.js"; // #100 worker 腿 prompts 边界归一(中立叶)
|
|
9
10
|
/** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
|
|
10
11
|
* payload that is not an effective config at all(非对象 / version 非有限数 / gate 域坏形——[2281] 裁B)。
|
|
11
12
|
* `domainErrors`:坏 catalog 域(schema default 已落)逐域单列——与本地腿 `FetchEffectiveResult` 同键同义,
|
|
@@ -35,6 +36,14 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
|
|
|
35
36
|
// 警告(值已被收编接受)不算 error,不进 domainErrors——与本地店同口径。
|
|
36
37
|
const warnings = [];
|
|
37
38
|
const wire = readEffectiveWire(await res.json(), (w) => warnings.push(w));
|
|
39
|
+
// #100 边界单点归一(§V2-B):worker 腿 prompts 键在**这里**定形——0.13.0 起 wire 值 verbatim 到手,
|
|
40
|
+
// 存储原文(global lane 合法下发形/emptyEffective 底座)归一为「键缺席」或 catalog 投影,下游
|
|
41
|
+
// boot/adopt/refresh/deferred/LKG 存取全部只见「缺席或消费形」(裸 `.prompts` 直读有棘轮钉拦)。
|
|
42
|
+
const centerPrompts = centerPromptsFromEffective(wire);
|
|
43
|
+
if (centerPrompts === undefined)
|
|
44
|
+
delete wire.prompts;
|
|
45
|
+
else
|
|
46
|
+
wire.prompts = centerPrompts;
|
|
38
47
|
const domainErrors = warnings
|
|
39
48
|
.filter((w) => w.kind === "domain-defaulted")
|
|
40
49
|
.map((w) => ({ domain: w.domain, error: (w.error instanceof Error ? w.error.message : String(w.error)).slice(0, 600) }));
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { skillContentHash } from "@sema-agent/registry-core";
|
|
9
9
|
import { fetchSkillContent } from "./http-client.js";
|
|
10
|
+
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
10
11
|
/**
|
|
11
12
|
* Merge center skills OVER the image baseline (design/41-sibling B1; `sema-registry docs/MCP-SKILLS.md`). Load order =
|
|
12
13
|
* `loadSkills(SKILLS_DIR)` baseline → center skills lazily fetched by hash and overlaid BY NAME (center
|
|
@@ -26,7 +27,7 @@ diskCacheDir) {
|
|
|
26
27
|
if (!diskCacheDir)
|
|
27
28
|
return undefined;
|
|
28
29
|
const hex = hash.replace(/^sha256:/, "");
|
|
29
|
-
if (
|
|
30
|
+
if (!SHA256_HEX_RE.test(hex))
|
|
30
31
|
return undefined; // hash 形不对=不碰盘(路径安全)
|
|
31
32
|
try {
|
|
32
33
|
const text = await fsp.readFile(joinPath(diskCacheDir, hex), "utf8");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/158 R12 收编叶子 — the sha256 content-address SHAPE constants, single-sourced. A neutral leaf (no
|
|
3
|
+
* imports of its own) so both `security.ts`-adjacent modules and prompt/image/session-sync consumers can
|
|
4
|
+
* depend on it without a layering edge. Six call sites across the tree each ran their OWN literal copy of one
|
|
5
|
+
* of these two regexes (a bare-hex form and a scheme-qualified form) — same shape, independently typed out.
|
|
6
|
+
* This module is the single point of truth for the SHAPE; each consumer's accept POLICY (prefix-stripping,
|
|
7
|
+
* case-folding order) is UNCHANGED by this collection — those policies differ across call sites (a real,
|
|
8
|
+
* pre-existing divergence, not introduced here) and are tracked for a follow-up cross-repo alignment, not
|
|
9
|
+
* unified in this refactor.
|
|
10
|
+
*/
|
|
11
|
+
/** A bare sha256 content-address: exactly 64 lowercase-hex characters, no scheme prefix. */
|
|
12
|
+
export declare const SHA256_HEX_RE: RegExp;
|
|
13
|
+
/** A scheme-qualified sha256 content-address: `sha256:` followed by 64 lowercase-hex characters. */
|
|
14
|
+
export declare const SHA256_DIGEST_RE: RegExp;
|
|
15
|
+
//# sourceMappingURL=digest-form.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/158 R12 收编叶子 — the sha256 content-address SHAPE constants, single-sourced. A neutral leaf (no
|
|
3
|
+
* imports of its own) so both `security.ts`-adjacent modules and prompt/image/session-sync consumers can
|
|
4
|
+
* depend on it without a layering edge. Six call sites across the tree each ran their OWN literal copy of one
|
|
5
|
+
* of these two regexes (a bare-hex form and a scheme-qualified form) — same shape, independently typed out.
|
|
6
|
+
* This module is the single point of truth for the SHAPE; each consumer's accept POLICY (prefix-stripping,
|
|
7
|
+
* case-folding order) is UNCHANGED by this collection — those policies differ across call sites (a real,
|
|
8
|
+
* pre-existing divergence, not introduced here) and are tracked for a follow-up cross-repo alignment, not
|
|
9
|
+
* unified in this refactor.
|
|
10
|
+
*/
|
|
11
|
+
/** A bare sha256 content-address: exactly 64 lowercase-hex characters, no scheme prefix. */
|
|
12
|
+
export const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
13
|
+
/** A scheme-qualified sha256 content-address: `sha256:` followed by 64 lowercase-hex characters. */
|
|
14
|
+
export const SHA256_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
//# sourceMappingURL=digest-form.js.map
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { IdempotencyCache } from "./idempotency.js";
|
|
|
22
22
|
import type { ImagesLocal } from "./routes/images.js";
|
|
23
23
|
import type { SessionsLocal } from "./routes/sessions.js";
|
|
24
24
|
import type { SessionSyncLocal } from "./routes/session-sync.js";
|
|
25
|
+
import type { VerifyRoundsSpec } from "./verify-rounds.js";
|
|
25
26
|
/** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。 */
|
|
26
27
|
export interface RunRegistry {
|
|
27
28
|
idemCache: IdempotencyCache<{
|
|
@@ -80,10 +81,7 @@ export interface RouteRequestState {
|
|
|
80
81
|
export interface PreparedTaskSubmission {
|
|
81
82
|
spec: TaskSpec;
|
|
82
83
|
auth?: RequestAuth;
|
|
83
|
-
verify?:
|
|
84
|
-
maxRounds: number;
|
|
85
|
-
costCeilingMicroUsd?: number;
|
|
86
|
-
};
|
|
84
|
+
verify?: VerifyRoundsSpec;
|
|
87
85
|
cascade?: boolean;
|
|
88
86
|
jobId?: string;
|
|
89
87
|
body: TaskRequestBody;
|
|
@@ -97,10 +95,7 @@ export interface DriveResumeArgs {
|
|
|
97
95
|
taskConfig: Omit<TaskSpec, "objective" | "sessionId">;
|
|
98
96
|
resumeObjective: string;
|
|
99
97
|
outcome: ResumeOutcome;
|
|
100
|
-
verifyRounds:
|
|
101
|
-
maxRounds: number;
|
|
102
|
-
costCeilingMicroUsd?: number;
|
|
103
|
-
} | undefined;
|
|
98
|
+
verifyRounds: VerifyRoundsSpec | undefined;
|
|
104
99
|
/** codex M2: side-effects that must land AFTER the markResuming CAS is WON (decide accepted, sibling races
|
|
105
100
|
* lost — never fires on the 409/404 paths) and BEFORE the model leg drives (so the resumed leg's own next
|
|
106
101
|
* ask sees them — the decide leg's exemption grant). Contract: must not throw (callers swallow internally);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canonicalToolName } from "@sema-agent/core";
|
|
2
|
-
import { principalFrom, verifyDirectDoorProof, MAX_APPROVAL_REASON_CHARS } from "../../security.js";
|
|
2
|
+
import { principalFrom, verifyDirectDoorProof, MAX_APPROVAL_REASON_CHARS, decodeCheckpointScope } from "../../security.js";
|
|
3
3
|
import { redactedPreview } from "../../trace/redact.js";
|
|
4
4
|
import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
|
|
5
5
|
import { sleep } from "../sse-log.js";
|
|
@@ -586,7 +586,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
586
586
|
// like null (open), as the sibling sites already do (the 5040/5599/5653 normalizations). Without this an
|
|
587
587
|
// anonymous single-user durable worker DEADLOCKS its own approvals (decide → 404 forever; workflow audit
|
|
588
588
|
// 2026-07-13, confirmed by both verify lenses).
|
|
589
|
-
const
|
|
589
|
+
const decodedCpScope = cpScope != null ? decodeCheckpointScope(cpScope) : undefined;
|
|
590
|
+
const ownsIt = cpScope == null || decodedCpScope === undefined || decodedCpScope === deciderPrincipal; // null/"_" = anonymous/dev (open)
|
|
590
591
|
if (!ownsIt) {
|
|
591
592
|
sendError(res, 404, "not_found.approval", "approval not found");
|
|
592
593
|
return;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
|
|
2
|
-
import { HttpError, verifiedPrincipal, isUuidV7 } from "../../security.js";
|
|
2
|
+
import { HttpError, verifiedPrincipal, isUuidV7, encodeCheckpointScope } from "../../security.js";
|
|
3
3
|
import { runInBackground } from "../../runs.js";
|
|
4
4
|
import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
|
|
5
5
|
import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
|
|
@@ -661,7 +661,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
661
661
|
const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
|
|
662
662
|
if (leafId === undefined || leafId === null)
|
|
663
663
|
return;
|
|
664
|
-
const cpScope = run.owner
|
|
664
|
+
const cpScope = encodeCheckpointScope(run.owner); // 匿名提交哨兵,与 main.ts putCtx 的 principal ?? "_" 同域
|
|
665
665
|
const wakeToken = mintCheckpointToken();
|
|
666
666
|
await cs.put(wakeToken, {
|
|
667
667
|
token: wakeToken,
|