@sema-agent/server 1.285.2 → 1.287.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/README.md +9 -4
- package/README.zh-CN.md +7 -4
- package/dist/orchestration/hardened-vm-runner.d.ts +2 -0
- package/dist/orchestration/hardened-vm-runner.js +45 -0
- package/dist/orchestration/workflow-completion-inbox.js +1 -1
- package/dist/plugins/background-agent-store-sql.js +1 -1
- package/dist/plugins/pg-image-bake.js +15 -15
- package/dist/plugins/pg-image-index.js +2 -2
- package/dist/plugins/roster-store-sql.js +3 -3
- package/dist/trace/project.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -82,10 +82,15 @@ Requirements: Node ≥ 20 (npm path) and an OpenAI-compatible model gateway.
|
|
|
82
82
|
> noise with no runtime exposure here**: `glob` has exactly one load site inside `e2b` — a `dynamicImport`
|
|
83
83
|
> in its *template-build* file-packing path — and this server only ever touches E2B's *sandbox runtime*
|
|
84
84
|
> API. Verified by execution, not by reading: importing `e2b`, constructing the adapter and driving a real
|
|
85
|
-
> `exec` never puts `glob` in the module cache. We
|
|
86
|
-
>
|
|
87
|
-
>
|
|
88
|
-
>
|
|
85
|
+
> `exec` never puts `glob` in the module cache. **We deliberately do not silence it.** The only thing that
|
|
86
|
+
> actually works is bundling e2b's whole subtree into this package (measured: the warning does go away) —
|
|
87
|
+
> and that costs 2.7 MB → 20.8 MB unpacked, marks the tree `invalid` in `npm ls`, and decouples the `e2b`
|
|
88
|
+
> you install from the one e2b publishes. Not worth it for a warning with no runtime reach. Things that
|
|
89
|
+
> do **not** work, in case you were about to try: our `overrides` (they only apply when the package.json
|
|
90
|
+
> being read *is the project npm was invoked on*), and a published `npm-shrinkwrap.json` (also ignored when
|
|
91
|
+
> this package is a dependency — both measured). If the warning bothers you, add
|
|
92
|
+
> `"overrides": { "glob": "^13" }` to **your own** project's package.json — that is the one npm reads, and
|
|
93
|
+
> `glob@13` is API-compatible with what e2b uses. The real fix is upstream in `e2b`.
|
|
89
94
|
|
|
90
95
|
```bash
|
|
91
96
|
# A) npm
|
package/README.zh-CN.md
CHANGED
|
@@ -78,10 +78,13 @@
|
|
|
78
78
|
> `e2b`(E2B 沙箱 SDK)间接引入。这是**安装期噪声,在这里没有任何运行时曝露面**:`glob` 在 `e2b` 里的
|
|
79
79
|
> 唯一加载点是它 *template-build* 打包路径里的一处 `dynamicImport`,而本服务只使用 E2B 的**沙箱运行时**
|
|
80
80
|
> 接口。这一条是**跑出来的、不是读代码推断的**:import `e2b`、构造适配器、真调一次 `exec`,全程 `glob`
|
|
81
|
-
>
|
|
82
|
-
> npm
|
|
83
|
-
>
|
|
84
|
-
>
|
|
81
|
+
> 从未进入模块缓存。**我们是刻意不消它的。** 唯一真能消掉的办法是把 e2b 整棵子树 bundle 进本包
|
|
82
|
+
> (实测有效,警告确实消失),代价是解包体积 2.7 MB → 20.8 MB、`npm ls` 会把依赖树标成 `invalid`、
|
|
83
|
+
> 且你装到的 `e2b` 与 e2b 官方发布的那份脱钩 —— 为一条没有运行时影响的警告付这些,不划算。
|
|
84
|
+
> **顺带告诉你哪些做法没用**(免得你去试):我们写的 `overrides`(只在"被读的那份 package.json 正是
|
|
85
|
+
> npm 本次调用的项目清单"时生效),以及随包发布的 `npm-shrinkwrap.json`(本包作为依赖被安装时同样被
|
|
86
|
+
> 忽略)—— 两条都是实测。若这条警告碍事,把 `"overrides": { "glob": "^13" }` 加进**你自己项目**的
|
|
87
|
+
> package.json(那份才是 npm 会读的;`glob@13` 与 e2b 用到的 API 兼容,已验)。真正的修复在上游 `e2b`。
|
|
85
88
|
|
|
86
89
|
```bash
|
|
87
90
|
# A) npm
|
|
@@ -6,6 +6,8 @@ export interface HardenedVmLimits {
|
|
|
6
6
|
concurrency?: number;
|
|
7
7
|
maxHeapMb?: number;
|
|
8
8
|
}
|
|
9
|
+
export declare function setScriptRealmRejectionObserver(observer: (summary: string) => void): void;
|
|
10
|
+
export declare function installScriptRealmRejectionGuard(): void;
|
|
9
11
|
export interface HardenedMembrane {
|
|
10
12
|
bridge(hostFn: (...args: unknown[]) => unknown): (...a: unknown[]) => Promise<unknown>;
|
|
11
13
|
dataIn(value: unknown): unknown;
|
|
@@ -24,6 +24,30 @@ function scrubProtoDeep(value) {
|
|
|
24
24
|
scrubProtoDeep(value[key]);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
let rejectionGuardInstalled = false;
|
|
28
|
+
let onScriptRealmRejection;
|
|
29
|
+
export function setScriptRealmRejectionObserver(observer) {
|
|
30
|
+
onScriptRealmRejection = observer;
|
|
31
|
+
}
|
|
32
|
+
export function installScriptRealmRejectionGuard() {
|
|
33
|
+
if (rejectionGuardInstalled)
|
|
34
|
+
return;
|
|
35
|
+
rejectionGuardInstalled = true;
|
|
36
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
37
|
+
if (!(promise instanceof Promise)) {
|
|
38
|
+
const summary = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
|
|
39
|
+
try {
|
|
40
|
+
(onScriptRealmRejection ?? ((s) => console.warn(`hardened-vm: un-awaited script rejection contained: ${s}`)))(summary);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (process.listenerCount("unhandledRejection") > 1)
|
|
47
|
+
return;
|
|
48
|
+
throw reason;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
27
51
|
function sterilizingParse(json) {
|
|
28
52
|
const parsed = JSON.parse(json);
|
|
29
53
|
scrubProtoDeep(parsed);
|
|
@@ -44,6 +68,7 @@ async function runHardened(opts) {
|
|
|
44
68
|
const { signal } = opts;
|
|
45
69
|
if (signal?.aborted)
|
|
46
70
|
throw new WorkflowScriptError("hardened-vm aborted before start");
|
|
71
|
+
installScriptRealmRejectionGuard();
|
|
47
72
|
const sandbox = { __proto__: null };
|
|
48
73
|
const ctx = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
49
74
|
vm.runInContext(`(() => {
|
|
@@ -73,7 +98,20 @@ async function runHardened(opts) {
|
|
|
73
98
|
if (r.ok) return r.v;
|
|
74
99
|
const e = new Error(r.err); e.name = r.name || "Error"; throw e;
|
|
75
100
|
})`, ctx);
|
|
101
|
+
let inFlightHostCalls = 0;
|
|
102
|
+
let scriptSettled = false;
|
|
103
|
+
let fireDeadlock;
|
|
104
|
+
const probeQuiescence = () => {
|
|
105
|
+
if (scriptSettled || inFlightHostCalls > 0)
|
|
106
|
+
return;
|
|
107
|
+
setImmediate(() => {
|
|
108
|
+
if (scriptSettled || inFlightHostCalls > 0)
|
|
109
|
+
return;
|
|
110
|
+
fireDeadlock?.();
|
|
111
|
+
});
|
|
112
|
+
};
|
|
76
113
|
const bridge = (hostFn) => mkBridge(async (argsJson) => {
|
|
114
|
+
inFlightHostCalls++;
|
|
77
115
|
try {
|
|
78
116
|
const out = await hostFn(...sterilizingParse(argsJson));
|
|
79
117
|
return JSON.stringify({ ok: true, v: out ?? null });
|
|
@@ -85,6 +123,10 @@ async function runHardened(opts) {
|
|
|
85
123
|
name: e instanceof Error ? e.name : "Error",
|
|
86
124
|
});
|
|
87
125
|
}
|
|
126
|
+
finally {
|
|
127
|
+
inFlightHostCalls--;
|
|
128
|
+
probeQuiescence();
|
|
129
|
+
}
|
|
88
130
|
});
|
|
89
131
|
const membrane = { bridge, dataIn, ctxEval: (src) => vm.runInContext(src, ctx) };
|
|
90
132
|
for (const [name, value] of Object.entries(opts.buildGlobals(membrane)))
|
|
@@ -97,6 +139,8 @@ async function runHardened(opts) {
|
|
|
97
139
|
throw new WorkflowScriptError(`script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
140
|
}
|
|
99
141
|
const scriptPromise = invocation.runInContext(ctx, { timeout: cfg.syncTimeoutMs });
|
|
142
|
+
scriptPromise.then(() => (scriptSettled = true), () => (scriptSettled = true));
|
|
143
|
+
probeQuiescence();
|
|
100
144
|
let timer;
|
|
101
145
|
let onAbort;
|
|
102
146
|
try {
|
|
@@ -109,6 +153,7 @@ async function runHardened(opts) {
|
|
|
109
153
|
onAbort = () => reject(new WorkflowScriptError("hardened-vm aborted"));
|
|
110
154
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
111
155
|
}
|
|
156
|
+
fireDeadlock = () => reject(new WorkflowScriptError("script deadlocked: no host call (agent/phase) in flight and nothing can resume the script — usually an un-awaited promise cycle, e.g. `const r = phase(...)` without await whose body reads `r`."));
|
|
112
157
|
}),
|
|
113
158
|
]);
|
|
114
159
|
return marshalOut(result);
|
|
@@ -268,7 +268,7 @@ export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPri
|
|
|
268
268
|
}
|
|
269
269
|
catch {
|
|
270
270
|
}
|
|
271
|
-
await emit({ type: "task_notification", task_id: e.runId, status: e.status, summary: e.summary, ...extras });
|
|
271
|
+
await emit({ type: "task_notification", task_id: e.runId, status: e.status, summary: e.summary, ...extras, injected: false });
|
|
272
272
|
}
|
|
273
273
|
else {
|
|
274
274
|
await emit({ type: "workflow_complete", runId: e.runId, status: e.status, summary: e.summary });
|
|
@@ -132,7 +132,7 @@ export async function ensurePgBackgroundAgentSchema(q) {
|
|
|
132
132
|
handle VARCHAR(190) COLLATE "C" NOT NULL,
|
|
133
133
|
scope VARCHAR(190) NOT NULL,
|
|
134
134
|
owner VARCHAR(190) NOT NULL,
|
|
135
|
-
session_scoped
|
|
135
|
+
session_scoped SMALLINT NOT NULL,
|
|
136
136
|
session_id VARCHAR(190),
|
|
137
137
|
parent_session_id VARCHAR(190),
|
|
138
138
|
-- core 1.367 δ(additive,TiDB 同案):root 锚(listBySession 第二臂)
|
|
@@ -7,9 +7,9 @@ export const PG_IMAGE_BAKE_SCHEMA = [
|
|
|
7
7
|
profile VARCHAR(128) NOT NULL,
|
|
8
8
|
bands JSONB NULL,
|
|
9
9
|
base_ref VARCHAR(255) NULL,
|
|
10
|
-
push
|
|
11
|
-
dry_run
|
|
12
|
-
logs
|
|
10
|
+
push SMALLINT NOT NULL DEFAULT 0,
|
|
11
|
+
dry_run SMALLINT NOT NULL DEFAULT 0,
|
|
12
|
+
logs SMALLINT NOT NULL DEFAULT 0,
|
|
13
13
|
argv JSONB NOT NULL,
|
|
14
14
|
status VARCHAR(16) NOT NULL,
|
|
15
15
|
state VARCHAR(16) NULL,
|
|
@@ -26,7 +26,7 @@ export const PG_IMAGE_BAKE_SCHEMA = [
|
|
|
26
26
|
ingest_secret VARCHAR(64) NULL,
|
|
27
27
|
runner_id VARCHAR(64) NULL,
|
|
28
28
|
lease_until TIMESTAMPTZ(3) NULL,
|
|
29
|
-
cancel_requested
|
|
29
|
+
cancel_requested SMALLINT NOT NULL DEFAULT 0,
|
|
30
30
|
requested_by VARCHAR(128) NULL,
|
|
31
31
|
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
32
32
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
@@ -141,14 +141,14 @@ export class PgImageBake {
|
|
|
141
141
|
try {
|
|
142
142
|
await this.pool.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
143
143
|
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
144
|
-
"VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,
|
|
144
|
+
"VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12)", [
|
|
145
145
|
bakeId,
|
|
146
146
|
input.profile,
|
|
147
147
|
input.bands == null ? null : JSON.stringify(input.bands),
|
|
148
148
|
input.baseRef,
|
|
149
|
-
input.push,
|
|
150
|
-
input.dryRun,
|
|
151
|
-
input.logs,
|
|
149
|
+
input.push ? 1 : 0,
|
|
150
|
+
input.dryRun ? 1 : 0,
|
|
151
|
+
input.logs ? 1 : 0,
|
|
152
152
|
JSON.stringify(input.argv),
|
|
153
153
|
input.idemKey,
|
|
154
154
|
input.requestedBy,
|
|
@@ -179,15 +179,15 @@ export class PgImageBake {
|
|
|
179
179
|
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
|
|
180
180
|
const res = await conn.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
181
181
|
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
182
|
-
"SELECT $1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,
|
|
183
|
-
"(SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run =
|
|
182
|
+
"SELECT $1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12 WHERE NOT EXISTS " +
|
|
183
|
+
"(SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0)", [
|
|
184
184
|
bakeId,
|
|
185
185
|
input.profile,
|
|
186
186
|
input.bands == null ? null : JSON.stringify(input.bands),
|
|
187
187
|
input.baseRef,
|
|
188
|
-
input.push,
|
|
189
|
-
input.dryRun,
|
|
190
|
-
input.logs,
|
|
188
|
+
input.push ? 1 : 0,
|
|
189
|
+
input.dryRun ? 1 : 0,
|
|
190
|
+
input.logs ? 1 : 0,
|
|
191
191
|
JSON.stringify(input.argv),
|
|
192
192
|
input.idemKey,
|
|
193
193
|
input.requestedBy,
|
|
@@ -229,7 +229,7 @@ export class PgImageBake {
|
|
|
229
229
|
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
230
230
|
}
|
|
231
231
|
async findActiveAny() {
|
|
232
|
-
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run =
|
|
232
|
+
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
|
|
233
233
|
"ORDER BY created_at ASC LIMIT 1");
|
|
234
234
|
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
235
235
|
}
|
|
@@ -413,7 +413,7 @@ export class PgImageBake {
|
|
|
413
413
|
return (leaseRes.rowCount ?? 0) >= 1;
|
|
414
414
|
}
|
|
415
415
|
async requestCancel(bakeId) {
|
|
416
|
-
const res = await this.pool.query("UPDATE image_bake SET cancel_requested =
|
|
416
|
+
const res = await this.pool.query("UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')", [new Date(), bakeId]);
|
|
417
417
|
return (res.rowCount ?? 0) > 0;
|
|
418
418
|
}
|
|
419
419
|
async isCancelRequested(bakeId) {
|
|
@@ -21,7 +21,7 @@ export const PG_IMAGE_INDEX_SCHEMA = [
|
|
|
21
21
|
generator_version VARCHAR(32),
|
|
22
22
|
build_date TIMESTAMPTZ(3),
|
|
23
23
|
supersedes VARCHAR(64),
|
|
24
|
-
signed
|
|
24
|
+
signed SMALLINT NOT NULL DEFAULT 0,
|
|
25
25
|
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
26
26
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
27
27
|
PRIMARY KEY (id)
|
|
@@ -129,7 +129,7 @@ function upsertParams(e, id, now) {
|
|
|
129
129
|
e.generatorVersion == null ? null : pgSanitizeText(e.generatorVersion),
|
|
130
130
|
e.buildDate ? new Date(e.buildDate) : null,
|
|
131
131
|
e.supersedes == null ? null : pgSanitizeText(e.supersedes),
|
|
132
|
-
e.signed ?
|
|
132
|
+
e.signed ? 1 : 0,
|
|
133
133
|
now,
|
|
134
134
|
now,
|
|
135
135
|
];
|
|
@@ -16,7 +16,7 @@ function rowToEntry(r) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
const RESOLVE_WHERE_TIDB = `name_key = ? AND BINARY scope = ? AND (BINARY owner = ? OR (session_scoped = 1 AND ? IS NOT NULL AND BINARY owner = ?))`;
|
|
19
|
-
const RESOLVE_WHERE_PG = `name_key = $1 AND scope = $2 AND (owner = $3 OR (session_scoped =
|
|
19
|
+
const RESOLVE_WHERE_PG = `name_key = $1 AND scope = $2 AND (owner = $3 OR (session_scoped = 1 AND $4::varchar IS NOT NULL AND owner = $5))`;
|
|
20
20
|
const COLS = "name, agent_id, session_id, tool_use_id, owner, scope, session_scoped, root_session_id, model, created_at_ms";
|
|
21
21
|
export async function ensureTiDBRosterSchema(pool) {
|
|
22
22
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${ROSTER_TABLE} (
|
|
@@ -53,7 +53,7 @@ export async function ensurePgRosterSchema(q) {
|
|
|
53
53
|
-- owner/scope:core 1.365 BREAKING 双轴必填(TiDB 同案;曾 nullable 的由来见两 ensure 上方顶注 ①)
|
|
54
54
|
owner VARCHAR(190) COLLATE "C" NOT NULL,
|
|
55
55
|
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
56
|
-
session_scoped
|
|
56
|
+
session_scoped SMALLINT,
|
|
57
57
|
-- core 1.367 δ(additive,TiDB 同案):RosterEntry.rootSessionId verbatim 存
|
|
58
58
|
root_session_id VARCHAR(190),
|
|
59
59
|
-- core 1.373 additive(TiDB 同案):解析后模型 id(NULL = 该 spawn 未记录模型)
|
|
@@ -75,7 +75,7 @@ function upsertParams(entry, now) {
|
|
|
75
75
|
entry.toolUseId ?? null,
|
|
76
76
|
entry.owner,
|
|
77
77
|
entry.scope,
|
|
78
|
-
entry.sessionScoped === undefined ? null : entry.sessionScoped,
|
|
78
|
+
entry.sessionScoped === undefined ? null : entry.sessionScoped ? 1 : 0,
|
|
79
79
|
entry.rootSessionId ?? null,
|
|
80
80
|
entry.model ?? null,
|
|
81
81
|
entry.createdAt,
|
package/dist/trace/project.js
CHANGED
|
@@ -82,6 +82,7 @@ export function taskProgressEventData(ev) {
|
|
|
82
82
|
export function taskNotificationEventData(ev) {
|
|
83
83
|
const n = ev.notification;
|
|
84
84
|
return {
|
|
85
|
+
injected: false,
|
|
85
86
|
task_id: n.task_id,
|
|
86
87
|
task_type: n.task_type,
|
|
87
88
|
...(n.sessionId !== undefined ? { sessionId: n.sessionId } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.287.0",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@sema-agent/core": "^1.
|
|
50
|
+
"@sema-agent/core": "^1.427.0",
|
|
51
51
|
"@sema-agent/registry-core": "^0.10.21",
|
|
52
52
|
"e2b": "^2.28.0",
|
|
53
53
|
"libsodium-wrappers": "^0.8.4",
|