@sema-agent/server 7.131.0 → 7.132.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 +309 -0
- package/README.md +5 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +163 -54
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +19 -47
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +6 -7
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/scenarios.d.ts +34 -40
- package/dist/capabilities/scenarios.js +16 -33
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/config-catalog.js +6 -6
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +12 -4
- package/dist/config-types.d.ts +7 -9
- package/dist/config.js +17 -4
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +78 -35
- package/dist/hooks/hook-runner.d.ts +232 -80
- package/dist/hooks/hook-runner.js +218 -312
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +101 -57
- package/dist/http/approval-sweeps.js +57 -16
- package/dist/http/checkpoint-ctx.d.ts +25 -8
- package/dist/http/checkpoint-ctx.js +11 -7
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +5 -9
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +43 -22
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +19 -9
- package/dist/http/send.d.ts +9 -7
- package/dist/http/send.js +7 -2
- package/dist/http/server.d.ts +21 -11
- package/dist/http/server.js +28 -2
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +1 -9
- package/dist/mcp-assertion-header.js +0 -6
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-route-endpoint.js +5 -5
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +42 -6
- package/dist/observability/fail-open.js +42 -6
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +137 -90
- package/dist/plugins/one-time-migrations.js +356 -207
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +2 -4
- package/dist/rule-entry-list.d.ts +12 -9
- package/dist/rule-entry-list.js +37 -21
- package/dist/run-local.js +19 -19
- package/dist/runs.d.ts +6 -0
- package/dist/runs.js +16 -0
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +28 -16
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-tool-axes-m19.d.ts +19 -4
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +46 -58
- package/dist/task-mcp.js +99 -65
- package/dist/task-settings.d.ts +7 -21
- package/dist/task-settings.js +7 -36
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.js +4 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +19 -6
- package/dist/trace/project.js +57 -22
- package/dist/trace/wire-projection-faces.d.ts +79 -1
- package/dist/trace/wire-projection-faces.js +22 -0
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +2 -3
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +34 -1
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
|
@@ -2,10 +2,12 @@ import path from "node:path";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { hostSubprocessEnv } from "../server-secret-env.js";
|
|
6
|
+
import { LANE_HOME_PROBE_COMMAND, judgeLaneHome } from "../lane-home-probe.js";
|
|
5
7
|
import { randomBytes } from "node:crypto";
|
|
6
8
|
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
7
9
|
import { signalNumber } from "./host-platform.js";
|
|
8
|
-
import {
|
|
10
|
+
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
9
11
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
10
12
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
11
13
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
@@ -30,6 +32,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
30
32
|
handle;
|
|
31
33
|
destroyed = false;
|
|
32
34
|
connecting;
|
|
35
|
+
homeRefusal;
|
|
33
36
|
constructor(config, deps = {}) {
|
|
34
37
|
if (!config.image || !config.image.trim()) {
|
|
35
38
|
config = { ...config, image: config.image ?? "" };
|
|
@@ -62,6 +65,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
62
65
|
return unsupported("connect(snapshotId)");
|
|
63
66
|
if (this.destroyed)
|
|
64
67
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
68
|
+
if (this.homeRefusal)
|
|
69
|
+
return { ok: false, error: this.homeRefusal };
|
|
65
70
|
if (config?.abortSignal?.aborted)
|
|
66
71
|
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
67
72
|
if (this.containerId)
|
|
@@ -112,12 +117,39 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
112
117
|
await this.forceRemove().catch(() => { });
|
|
113
118
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
114
119
|
}
|
|
120
|
+
if (this.homeDir !== undefined && !(await this.untilOrAborted(this.probeContainerShell(), config?.abortSignal))) {
|
|
121
|
+
await this.forceRemove().catch(() => { });
|
|
122
|
+
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while detecting the container's shell") };
|
|
123
|
+
}
|
|
124
|
+
const home = await this.homeVerdict(config?.abortSignal);
|
|
125
|
+
if (home !== undefined) {
|
|
126
|
+
await this.forceRemove().catch(() => { });
|
|
127
|
+
return { ok: false, error: home };
|
|
128
|
+
}
|
|
129
|
+
if (this.destroyed) {
|
|
130
|
+
await this.forceRemove().catch(() => { });
|
|
131
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
132
|
+
}
|
|
115
133
|
this.containerId = run.value.stdout.trim() || this.containerName;
|
|
116
134
|
this.handle = { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
117
135
|
if (config?.secrets?.length) {
|
|
118
136
|
}
|
|
119
137
|
return ok(this.handle);
|
|
120
138
|
}
|
|
139
|
+
async homeVerdict(signal) {
|
|
140
|
+
if (this.homeDir === undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
const probe = await this.docker(this.execRaw(LANE_HOME_PROBE_COMMAND), { signal, timeoutMs: this.cfg.controlTimeoutMs });
|
|
143
|
+
const verdict = judgeLaneHome(PROVIDER, this.homeDir, probe.ok && probe.value.exitCode === 0 ? { ok: true, stdout: probe.value.stdout } : { ok: false, reason: probe.ok ? `exit ${probe.value.exitCode}` : probe.error.message });
|
|
144
|
+
if (verdict === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (!probe.ok && probe.error.code === "aborted")
|
|
147
|
+
return new RemoteExecutionError("aborted", "connect aborted while reading the environment's home");
|
|
148
|
+
const error = new RemoteExecutionError("connect_failed", verdict.refusal);
|
|
149
|
+
if (verdict.answered)
|
|
150
|
+
this.homeRefusal = error;
|
|
151
|
+
return error;
|
|
152
|
+
}
|
|
121
153
|
async suspendVM(_options) {
|
|
122
154
|
return unsupported("suspendVM");
|
|
123
155
|
}
|
|
@@ -130,11 +162,25 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
130
162
|
async reconnect(sessionToken) {
|
|
131
163
|
if (this.destroyed)
|
|
132
164
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
|
|
165
|
+
if (this.homeRefusal)
|
|
166
|
+
return { ok: false, error: this.homeRefusal };
|
|
133
167
|
if (sessionToken && sessionToken !== this.containerName) {
|
|
134
168
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `reconnect token '${String(sessionToken)}' does not match this env's container '${this.containerName}'`) };
|
|
135
169
|
}
|
|
170
|
+
if (this.connecting !== undefined && this.containerId === undefined) {
|
|
171
|
+
const joined = await this.connecting;
|
|
172
|
+
if (!joined.ok || this.containerId !== undefined)
|
|
173
|
+
return joined;
|
|
174
|
+
}
|
|
136
175
|
const insp = await this.docker(["inspect", "-f", "{{.State.Running}}", this.containerName], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
137
176
|
if (insp.ok && insp.value.exitCode === 0 && insp.value.stdout.trim() === "true") {
|
|
177
|
+
if (this.containerId === undefined && this.homeDir !== undefined)
|
|
178
|
+
await this.probeContainerShell();
|
|
179
|
+
const home = this.containerId === undefined ? await this.homeVerdict() : undefined;
|
|
180
|
+
if (home !== undefined)
|
|
181
|
+
return { ok: false, error: home };
|
|
182
|
+
if (this.destroyed || this.homeRefusal)
|
|
183
|
+
return { ok: false, error: this.homeRefusal ?? new RemoteExecutionError("connect_failed", "execution env destroyed during reconnect") };
|
|
138
184
|
this.containerId = this.containerName;
|
|
139
185
|
this.handle ??= { sandboxId: this.containerName, provider: PROVIDER, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
140
186
|
return ok(this.handle);
|
|
@@ -179,7 +225,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
179
225
|
if (!ready.ok)
|
|
180
226
|
throw new RemoteExecutionError("connect_failed", ready.error.message, ready.error);
|
|
181
227
|
const execArgs = this.execArgv(command, options?.cwd, options?.env);
|
|
182
|
-
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(execArgs), { stdio: ["ignore", "pipe", "pipe"] });
|
|
228
|
+
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(execArgs), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
183
229
|
const queue = [];
|
|
184
230
|
let finished = false;
|
|
185
231
|
let failure;
|
|
@@ -286,7 +332,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
286
332
|
child.kill("SIGKILL");
|
|
287
333
|
}
|
|
288
334
|
catch { }
|
|
289
|
-
|
|
335
|
+
const cut = buildLivenessCut(liveness);
|
|
336
|
+
throw new RemoteExecutionError(cut.code, cut.message);
|
|
290
337
|
}
|
|
291
338
|
}
|
|
292
339
|
while (queue.length)
|
|
@@ -427,6 +474,9 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
427
474
|
async createTempFile(options) {
|
|
428
475
|
return this.posixFs.createTempFile(options);
|
|
429
476
|
}
|
|
477
|
+
cliEnv() {
|
|
478
|
+
return hostSubprocessEnv({ site: "server.remote-env-local-docker.spawn-env", policy: "all", allow: [] });
|
|
479
|
+
}
|
|
430
480
|
withHostFlag(args) {
|
|
431
481
|
return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
|
|
432
482
|
}
|
|
@@ -447,7 +497,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
447
497
|
return new Promise((resolve) => {
|
|
448
498
|
let settled = false;
|
|
449
499
|
let forceSettleTimer;
|
|
450
|
-
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"] });
|
|
500
|
+
const child = this.spawnFn(this.cfg.dockerPath, this.withHostFlag(args), { stdio: ["ignore", "pipe", "pipe"], env: this.cliEnv() });
|
|
451
501
|
const killGrace = () => {
|
|
452
502
|
try {
|
|
453
503
|
child.kill("SIGKILL");
|
|
@@ -505,11 +555,29 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
505
555
|
});
|
|
506
556
|
}
|
|
507
557
|
containerShell = "/bin/sh";
|
|
508
|
-
|
|
509
|
-
async
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
558
|
+
containerShellReady;
|
|
559
|
+
async untilOrAborted(shared, signal) {
|
|
560
|
+
if (!signal) {
|
|
561
|
+
await shared;
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
if (signal.aborted)
|
|
565
|
+
return false;
|
|
566
|
+
let onAbort;
|
|
567
|
+
const aborted = new Promise((res) => {
|
|
568
|
+
onAbort = () => res(false);
|
|
569
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
570
|
+
});
|
|
571
|
+
const won = await Promise.race([shared.then(() => true), aborted]);
|
|
572
|
+
if (onAbort)
|
|
573
|
+
signal.removeEventListener("abort", onAbort);
|
|
574
|
+
return won;
|
|
575
|
+
}
|
|
576
|
+
probeContainerShell() {
|
|
577
|
+
this.containerShellReady ??= this.detectContainerShell();
|
|
578
|
+
return this.containerShellReady;
|
|
579
|
+
}
|
|
580
|
+
async detectContainerShell() {
|
|
513
581
|
try {
|
|
514
582
|
const r = await this.docker(["exec", this.containerName, "/bin/sh", "-c", "command -v bash"], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
515
583
|
if (r.ok && r.value.exitCode === 0 && r.value.stdout.trim().length > 0)
|
|
@@ -94,6 +94,10 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
94
94
|
private inFlight?;
|
|
95
95
|
/** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
|
|
96
96
|
readonly homeDir?: string | undefined;
|
|
97
|
+
/** 家目录声明 × 实测的判定,按连接记(同一条连接只问一次;重连换了连接再问 —— 登录用户不变时答案相同,代价一次往返)。 */
|
|
98
|
+
private homeChecked?;
|
|
99
|
+
/** 目标机答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每个面原样回它(同一目标机同一登录用户同一答案:不再问、不再发命令)。 */
|
|
100
|
+
private homeRefusal?;
|
|
97
101
|
/** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在远端登录 shell 里(目标机的 profile / env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
|
|
98
102
|
* 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
|
|
99
103
|
readonly cdPath: "unknown";
|
|
@@ -119,6 +123,17 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
119
123
|
* caller 的 signal 只中止**它自己的等待**(共享在飞尝试继续伺服其他等待者;彻底取消归 destroy 的代际
|
|
120
124
|
* 闸管,handshake 余命 ≤ readyTimeout 有界)。 */
|
|
121
125
|
private ensureConnected;
|
|
126
|
+
/** 家目录判定的 per-caller 可中止等待(共享的判定承诺不动,只退本 caller)。 */
|
|
127
|
+
private raceVerdictAbort;
|
|
128
|
+
/**
|
|
129
|
+
* 家目录:声明在场(`homeDir`,只来自 `REMOTE_EXEC_HOME_DIR` —— 本车道没有自己的缺省)⇒ 在这条连接上问一次 `$HOME`(登录用户的 shell 答的,
|
|
130
|
+
* 与命令同一个用户、同一份登录 env),交唯一判官 `judgeLaneHome`。答了而不一致 / 不是绝对路径 ⇒ 拒记在环境上(每个面原样回它),命令一条都不发;
|
|
131
|
+
* 没答上(通道开不出 / 非零退出 / 超时)⇒ 这一次同样不开、不记,下一次调用重问。
|
|
132
|
+
* 问的是裸命令(不经工作目录守卫:工作目录进不去不影响家目录的答案)。上界 = 握手的 `readyTimeoutMs`(问一个环境变量,不是跑用户命令)。
|
|
133
|
+
*/
|
|
134
|
+
private homeVerdict;
|
|
135
|
+
/** 连接本身(不含家目录判定):{@link ensureConnected} 的第一半。 */
|
|
136
|
+
private ensureTransport;
|
|
122
137
|
private connectOnce;
|
|
123
138
|
suspendVM(_options?: VmLifecycleOptions): Promise<{
|
|
124
139
|
ok: true;
|
|
@@ -3,10 +3,11 @@ import { Client } from "ssh2";
|
|
|
3
3
|
import { createSshHostKeyVerifier } from "../ssh-host-key.js";
|
|
4
4
|
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode } from "./remote-shell.js";
|
|
5
5
|
import { signalNumber } from "./host-platform.js";
|
|
6
|
-
import {
|
|
6
|
+
import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
|
|
7
7
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
8
8
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
9
9
|
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
10
|
+
import { LANE_HOME_PROBE_COMMAND, judgeLaneHome } from "../lane-home-probe.js";
|
|
10
11
|
const PROVIDER = "ssh";
|
|
11
12
|
const ok = (value) => ({ ok: true, value });
|
|
12
13
|
const unsupported = (op) => ({
|
|
@@ -25,6 +26,8 @@ export class RemoteSshExecutionEnv {
|
|
|
25
26
|
connEpoch = 0;
|
|
26
27
|
inFlight;
|
|
27
28
|
homeDir;
|
|
29
|
+
homeChecked;
|
|
30
|
+
homeRefusal;
|
|
28
31
|
cdPath = "unknown";
|
|
29
32
|
cfg;
|
|
30
33
|
injectedConnectOnce;
|
|
@@ -103,6 +106,107 @@ export class RemoteSshExecutionEnv {
|
|
|
103
106
|
return record.promise;
|
|
104
107
|
}
|
|
105
108
|
async ensureConnected(signal) {
|
|
109
|
+
for (let round = 0; round < 3; round++) {
|
|
110
|
+
if (this.homeRefusal)
|
|
111
|
+
return { ok: false, error: this.homeRefusal };
|
|
112
|
+
const transport = await this.ensureTransport(signal);
|
|
113
|
+
if (!transport.ok || this.homeDir === undefined)
|
|
114
|
+
return transport;
|
|
115
|
+
const conn = this.conn;
|
|
116
|
+
if (!conn)
|
|
117
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection dropped right after connect") };
|
|
118
|
+
if (this.homeChecked?.conn !== conn) {
|
|
119
|
+
const checked = { conn, verdict: this.homeVerdict(conn, this.homeDir) };
|
|
120
|
+
this.homeChecked = checked;
|
|
121
|
+
void checked.verdict.then((r) => {
|
|
122
|
+
if (!r.ok && this.homeRefusal === undefined && this.homeChecked === checked)
|
|
123
|
+
this.homeChecked = undefined;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const verdict = await this.raceVerdictAbort(this.homeChecked.verdict, signal);
|
|
127
|
+
if (signal?.aborted)
|
|
128
|
+
return { ok: false, error: new ExecutionError("aborted", "aborted while checking the environment's home") };
|
|
129
|
+
if (!verdict.ok)
|
|
130
|
+
return verdict;
|
|
131
|
+
if (this.conn === conn)
|
|
132
|
+
return verdict;
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH connection kept being replaced while the environment's home was being checked") };
|
|
135
|
+
}
|
|
136
|
+
async raceVerdictAbort(shared, signal) {
|
|
137
|
+
if (!signal)
|
|
138
|
+
return shared;
|
|
139
|
+
if (signal.aborted)
|
|
140
|
+
return { ok: false, error: new ExecutionError("aborted", "aborted while checking the environment's home") };
|
|
141
|
+
let onAbort;
|
|
142
|
+
const aborted = new Promise((res) => {
|
|
143
|
+
onAbort = () => res({ ok: false, error: new ExecutionError("aborted", "aborted while checking the environment's home") });
|
|
144
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
});
|
|
146
|
+
const raced = await Promise.race([shared, aborted]);
|
|
147
|
+
if (onAbort)
|
|
148
|
+
signal.removeEventListener("abort", onAbort);
|
|
149
|
+
return raced;
|
|
150
|
+
}
|
|
151
|
+
homeVerdict(conn, declared) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let out = "";
|
|
154
|
+
let settled = false;
|
|
155
|
+
let channel;
|
|
156
|
+
let channelClosed = false;
|
|
157
|
+
const closeChannel = (ch) => {
|
|
158
|
+
if (ch === undefined || channelClosed)
|
|
159
|
+
return undefined;
|
|
160
|
+
try {
|
|
161
|
+
ch.close();
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return `the probe channel could not be closed: ${e instanceof Error ? e.message : String(e)}`;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const settle = (probe) => {
|
|
169
|
+
if (settled)
|
|
170
|
+
return;
|
|
171
|
+
settled = true;
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
const closeTrouble = closeChannel(channel);
|
|
174
|
+
if (closeTrouble !== undefined && !probe.ok)
|
|
175
|
+
probe = { ok: false, reason: `${probe.reason}; ${closeTrouble}` };
|
|
176
|
+
const verdict = judgeLaneHome(PROVIDER, declared, probe);
|
|
177
|
+
if (verdict === undefined)
|
|
178
|
+
return resolve(ok(undefined));
|
|
179
|
+
const error = new ExecutionError("shell_unavailable", verdict.refusal);
|
|
180
|
+
if (verdict.answered)
|
|
181
|
+
this.homeRefusal = error;
|
|
182
|
+
resolve({ ok: false, error });
|
|
183
|
+
};
|
|
184
|
+
const timer = setTimeout(() => settle({ ok: false, reason: `no answer within ${this.cfg.readyTimeoutMs}ms` }), this.cfg.readyTimeoutMs);
|
|
185
|
+
try {
|
|
186
|
+
conn.exec(LANE_HOME_PROBE_COMMAND, (err, stream) => {
|
|
187
|
+
if (err)
|
|
188
|
+
return settle({ ok: false, reason: err.message });
|
|
189
|
+
if (settled)
|
|
190
|
+
return void closeChannel(stream);
|
|
191
|
+
channel = stream;
|
|
192
|
+
stream
|
|
193
|
+
.on("data", (d) => {
|
|
194
|
+
if (out.length < 4096)
|
|
195
|
+
out += d.toString("utf8");
|
|
196
|
+
})
|
|
197
|
+
.on("close", (code) => {
|
|
198
|
+
channelClosed = true;
|
|
199
|
+
settle(code === 0 ? { ok: true, stdout: out } : { ok: false, reason: code == null ? "the channel closed without an exit status" : `exit ${code}` });
|
|
200
|
+
});
|
|
201
|
+
stream.stderr.on("data", () => { });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
settle({ ok: false, reason: e instanceof Error ? e.message : String(e) });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async ensureTransport(signal) {
|
|
106
210
|
if (this.destroyed)
|
|
107
211
|
return { ok: false, error: new ExecutionError("shell_unavailable", "SSH env destroyed — no lazy reconnect after teardown") };
|
|
108
212
|
if (signal?.aborted)
|
|
@@ -435,8 +539,10 @@ export class RemoteSshExecutionEnv {
|
|
|
435
539
|
if (waitTimer !== undefined)
|
|
436
540
|
clearTimeout(waitTimer);
|
|
437
541
|
}
|
|
438
|
-
if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms)
|
|
439
|
-
|
|
542
|
+
if (liveness !== undefined && !finished && Date.now() - lastChunk >= liveness.ms) {
|
|
543
|
+
const cut = buildLivenessCut(liveness);
|
|
544
|
+
throw new RemoteExecutionError(cut.code, cut.message);
|
|
545
|
+
}
|
|
440
546
|
}
|
|
441
547
|
while (queue.length)
|
|
442
548
|
yield queue.shift();
|
|
@@ -134,7 +134,6 @@ export declare function drainNumEnvWarnings(): Array<{
|
|
|
134
134
|
raw: string;
|
|
135
135
|
}>;
|
|
136
136
|
export declare function numEnvOr(name: string, def: number, min: number): number;
|
|
137
|
-
export declare const SYMLINK_CHAIN_MAX_HOPS = 64;
|
|
138
137
|
/**
|
|
139
138
|
* 一次往返走完叶 symlink 链的 POSIX sh 脚本(**纯数据**:返回一个命令串 ⇒ `build*`,CLAUDE.md 工厂律)。
|
|
140
139
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExecutionError } from "@sema-agent/core";
|
|
1
|
+
import { ExecutionError, SYMLINK_CHAIN_MAX_HOPS } from "@sema-agent/core";
|
|
2
2
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
export const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
@@ -120,7 +120,6 @@ export function numEnvOr(name, def, min) {
|
|
|
120
120
|
}
|
|
121
121
|
return Math.max(min, (Number.isFinite(n) ? n : 0) || def);
|
|
122
122
|
}
|
|
123
|
-
export const SYMLINK_CHAIN_MAX_HOPS = 64;
|
|
124
123
|
export function buildSymlinkChainCommand(absPath) {
|
|
125
124
|
return (`{ if ! command -v readlink >/dev/null 2>&1; then printf 'readlink: not found\\n' >&2; exit 127; fi; ` +
|
|
126
125
|
`c=${shellQuote(absPath)}; n=0; ` +
|
|
@@ -2,6 +2,7 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
|
2
2
|
import { TASK_LIST_ITEM_TABLE, TASK_LIST_META_TABLE, taskListKeyFor } from "./task-list-store-sql.js";
|
|
3
3
|
import { USER_INPUT_ACCEPTANCE_TABLE } from "./user-input-ledger-sql.js";
|
|
4
4
|
import { ensureIndexes } from "./ensure-index.js";
|
|
5
|
+
import { incarnationFromCell } from "../session-incarnation.js";
|
|
5
6
|
export const MANAGED_RETENTION = "managed";
|
|
6
7
|
export const UNMANAGED_RETENTION = "none";
|
|
7
8
|
export const RETENTION_AUDIT_TABLE = "retention_audit";
|
|
@@ -302,7 +303,10 @@ export class SqlRetentionStore {
|
|
|
302
303
|
const { rows } = await exec.query(this.q(`SELECT sm.session_id, sm.incarnation FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < ? AND NOT ${this.liveReferenceExists()} ` +
|
|
303
304
|
`ORDER BY sm.updated_at ASC LIMIT ?${suffix}`, `SELECT sm.session_id, sm.incarnation FROM session_meta sm WHERE ${w.sql} AND sm.updated_at < $2 AND NOT ${this.liveReferenceExists()} ` +
|
|
304
305
|
`ORDER BY sm.updated_at ASC LIMIT $3${suffix}`), [...w.params, new Date(input.cutoffMs), RETENTION_SESSION_BATCH]);
|
|
305
|
-
return rows.map((r) =>
|
|
306
|
+
return rows.map((r) => {
|
|
307
|
+
const id = String(r.session_id);
|
|
308
|
+
return { id, incarnation: incarnationFromCell(id, r.incarnation, "absent") };
|
|
309
|
+
});
|
|
306
310
|
}
|
|
307
311
|
async countOrphanToolResultCandidates(exec, input) {
|
|
308
312
|
return (await this.orphanToolResultCandidates(exec, input)).length;
|
|
@@ -337,7 +341,7 @@ export class SqlRetentionStore {
|
|
|
337
341
|
tombstones = await this.writeTombstones(conn, input.domain, "session", ids);
|
|
338
342
|
}
|
|
339
343
|
await this.writeAuditRow(conn, { domain: input.domain, action: "deleted_sessions", audit, deleted, skipped, tombstones });
|
|
340
|
-
const endedSpaces = doomed.
|
|
344
|
+
const endedSpaces = doomed.flatMap((d) => (d.incarnation === undefined ? [] : [{ sessionId: d.id, spaceKey: d.incarnation }]));
|
|
341
345
|
return { domain: input.domain, deleted, skipped, tombstones, ...(endedSpaces.length > 0 ? { endedSpaces } : {}) };
|
|
342
346
|
});
|
|
343
347
|
}
|
|
@@ -30,9 +30,29 @@
|
|
|
30
30
|
* information_schema 查询抛错、或整库一行都读不出来 ⇒ **拒启**。探针的语义是「我证明了库等于代码」,
|
|
31
31
|
* 证不出来时放行等于把一条保护性断言静默降级成祝福([ref]:保护型缺席必须 fail-closed)。
|
|
32
32
|
* `recordFailOpen` 在这条腿上**不适用** —— 它是 F 类(装饰/缓存)的登记口,不是 P 类保护臂的逃生门。
|
|
33
|
+
*
|
|
34
|
+
* ── 第二道对账:跳版升级的库带着**退役** schema(先于建表)──────────────────────────────────────
|
|
35
|
+
* 一次性迁移 M1–M3(7.74.0–7.125.x)删掉了整树快照索引表 `snapshot_manifest` 与单座 steering 列 `checkpoint.pending_steer`,并把账本 / 已决行里
|
|
36
|
+
* 旧拼法 `unresolvable` 改写成 `ancestor_marked`;三条窗关后已删码。从没在带那三条的版本上起过服的库(跳版升级)两件退役 schema 至少一件仍在 ⇒
|
|
37
|
+
* 旧拼法的改写同样没跑过 ⇒ {@link assertRetiredSchemaAbsent} 响亮拒启({@link RetiredSchemaPresentError},同一个拒启码 `config.schema_mismatch`),
|
|
38
|
+
* 点名在场的坐标与出路(先在 7.125.x 起服一次让那三条跑完,再升)。**旧拼法不另扫**:那是一条对账本 `task_event` 的无索引整表扫(全站最大的表,
|
|
39
|
+
* PG 池级 `statement_timeout` 会把它切成拒启、每次起服重演);两件 schema 的在场就是「这只库没经过带迁移的版本」的判据(7.74.0 之前的每一版 DDL
|
|
40
|
+
* 都建它们,7.74.0 起都不再建;能写下旧拼法的 7.64.0–7.71.0 恒带两者)。
|
|
41
|
+
* 这一道**没有窗**(跳版升级的库永远可能出现),所以它住在起服对账一族而不在一次性迁移文件里 —— 那个文件里每一条都有窗,最后一条删掉时整个文件
|
|
42
|
+
* 一起删、零例外。判据读本族**唯一**的列册读者 {@link readLiveColumns}(PG 读查询真正会解析到的关系,与第一道同一读法),不另造表 / 列在场探针。
|
|
43
|
+
* **先于一切 DDL**:唯一的读处是探针口 {@link assertRetiredSchemaAbsentOnPool},唯一的调用点是 `store-backend.ts` 里 SQL 两后端的 `ensureSchema()` 包装
|
|
44
|
+
* (探针先于它的第一条 CREATE)—— 一次会拒的起服不许先在库里建出几十张本版新表(`task_run` 还带着新列),否则拒句里「没动过库」就是假话,
|
|
45
|
+
* 按拒句指路去 7.125.x 起服的人会撞上「库比 7.125.x 新」的第二道拒。嵌入方走公开序列 `createStoreBackend()` → `ensureSchema()` → `runOneTimeMigrations()`
|
|
46
|
+
* 即得到这一问,零动作。
|
|
47
|
+
* 残余(如实):运维在第一次起带迁移的版本之前就把两件退役 schema 都手工删掉的库,残留的旧拼法由门记录的读腿接住 —— 7.129.0 起(core 7.51.0 #1731)
|
|
48
|
+
* 那只读腿按 core 的档案读法读,`unresolvable` 是 core 退役词注册表里的词 ⇒ 记录原词原样读回、不改写(`trace/project.ts` `screenedGate`);local 文件车道
|
|
49
|
+
* 没有表与列,没有这一问 —— 引擎 File 店里已决 checkpoint 的记录按契约物理不可变,本仓没有合法缝改写它们,同由那条读腿接住。
|
|
33
50
|
*/
|
|
51
|
+
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
52
|
+
import type { Pool as PgPool } from "pg";
|
|
34
53
|
import { type SchemaDialect } from "./schema-ensure-legs.js";
|
|
35
54
|
import { type SchemaProbeRead } from "./schema-live-columns.js";
|
|
55
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
36
56
|
/** 一列:名字 + 它在 `CREATE TABLE` 里的**原样定义文本**(生成 ADD COLUMN 指路用,不另手写)。 */
|
|
37
57
|
export interface ParsedColumn {
|
|
38
58
|
readonly name: string;
|
|
@@ -73,4 +93,38 @@ export declare function describeSchemaMismatch(dialect: SchemaDialect, mismatche
|
|
|
73
93
|
* 失败方向见文件头注:读抛 / 整库零行 ⇒ 拒。
|
|
74
94
|
*/
|
|
75
95
|
export declare function assertSchemaMatchesCode(read: SchemaProbeRead, dialect: SchemaDialect): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* 跳版升级的库的拒启:退役 schema(快照索引表 / 单座列)任一仍在。**具名类型**:锚类型不锚文案(日志消费端与测试按 `code` / 类型分派);
|
|
98
|
+
* 码取列册对账同一个 `config.schema_mismatch`,运维面一个拒启码一个读法。拒句点名在场的坐标、说明第三条(旧拼法改写)同样没跑过、给出路。
|
|
99
|
+
* 继承 {@link StoreBootRefusalError}:这是一句**确定**的拒启表态(再等也等不来另一个答案)—— `ensureSchema()` 里它不被 SQL 开库的等待窗重试、不被改写成「库打不开」的拒句。
|
|
100
|
+
*/
|
|
101
|
+
export declare class RetiredSchemaPresentError extends StoreBootRefusalError {
|
|
102
|
+
readonly found: readonly string[];
|
|
103
|
+
/** 机读码(与列册对账的拒启同一个)。 */
|
|
104
|
+
readonly code = "config.schema_mismatch";
|
|
105
|
+
constructor(found: readonly string[]);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 退役 schema 的判据(纯读,**唯一一只**;规则见文件头「第二道对账」一节):经列册读者 {@link readLiveColumns} 读一次库里的列集,
|
|
109
|
+
* 快照索引表 / 单座列任一在场 ⇒ 抛 {@link RetiredSchemaPresentError}。新库(建表之前一列都没有)⇒ 不在场、过。读抛 ⇒ 原样上抛(拒启;不是确定的拒启表态,
|
|
110
|
+
* 开库等待窗照常重试 —— 与修前两条目录探针抛出时同一个去向)。
|
|
111
|
+
*/
|
|
112
|
+
export declare function assertRetiredSchemaAbsent(read: SchemaProbeRead, dialect: SchemaDialect): Promise<void>;
|
|
113
|
+
/** 探针口的池参数:方言与它的池一起走(判别联合 ⇒ 不靠调用方断言「这个池是哪种」)。 */
|
|
114
|
+
export type RetiredSchemaProbeTarget = {
|
|
115
|
+
readonly dialect: "tidb";
|
|
116
|
+
readonly pool: MysqlPool;
|
|
117
|
+
} | {
|
|
118
|
+
readonly dialect: "pg";
|
|
119
|
+
readonly pool: PgPool;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* **探针口**(退役 schema 唯一的读处):库一连上、**任何 DDL 之前**在一条借来的连接上读一遍,在场 ⇒ 抛 {@link RetiredSchemaPresentError}。本仓唯一调用点 =
|
|
123
|
+
* `store-backend.ts` 的 `withRetiredSchemaProbe`(SQL 两后端的 `ensureSchema()` 自身:探针先于建表的先后只写在那一处)。**不在包根上**:
|
|
124
|
+
* 嵌入方经公开序列的 `ensureSchema()` 得到这一问,不需要自己排序列。
|
|
125
|
+
*
|
|
126
|
+
* 不取 `ensureSchema` 那把 advisory 锁:本口纯读(列册读),不改任何东西,与别的副本正在跑的建表 / 迁移互不相干 —— 退役 schema 只会被 7.125.x 的迁移删去、
|
|
127
|
+
* 不会被谁凭空建回。用完即还连接,拒启时连接也已归还。
|
|
128
|
+
*/
|
|
129
|
+
export declare function assertRetiredSchemaAbsentOnPool(target: RetiredSchemaProbeTarget): Promise<void>;
|
|
76
130
|
//# sourceMappingURL=schema-consistency.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordEnsureStatements } from "./schema-ensure-legs.js";
|
|
2
2
|
import { readLiveColumns } from "./schema-live-columns.js";
|
|
3
|
+
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
3
4
|
const QUOTES = new Set(["'", '"', "`"]);
|
|
4
5
|
function stripLineComments(sql) {
|
|
5
6
|
let out = "";
|
|
@@ -159,4 +160,55 @@ export async function assertSchemaMatchesCode(read, dialect) {
|
|
|
159
160
|
if (mismatches.length > 0)
|
|
160
161
|
throw new Error(describeSchemaMismatch(dialect, mismatches));
|
|
161
162
|
}
|
|
163
|
+
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
164
|
+
const RETIRED_STEER_SEAT = { table: "checkpoint", column: "pending_steer" };
|
|
165
|
+
export class RetiredSchemaPresentError extends StoreBootRefusalError {
|
|
166
|
+
found;
|
|
167
|
+
code = SCHEMA_MISMATCH_ERROR_CODE;
|
|
168
|
+
constructor(found) {
|
|
169
|
+
super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database still carries schema that the one-time migrations of server 7.74.0–7.125.x retired — refusing to start. ` +
|
|
170
|
+
`Found: ${found.join("; ")}. ` +
|
|
171
|
+
`Both come from builds that predate those migrations, so this database never booted a build that still carried them — which also means the third one never ran: ` +
|
|
172
|
+
`ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
|
|
173
|
+
`This build carries none of the three and will not guess. ` +
|
|
174
|
+
`Way out: start a server 7.125.x build once against this database and let its one-time migrations finish (it drops the table and the column and rewrites the spelling; ` +
|
|
175
|
+
`while the column still holds a steering instruction that can be delivered it refuses and names the rows and its own ways out), then upgrade to this build. ` +
|
|
176
|
+
`On PostgreSQL this check reads the relation an unqualified name resolves to along search_path, while the 7.125.x migrations look only at current_schema(): ` +
|
|
177
|
+
`if the table or the checkpoint table carrying the column lives in a schema later on search_path, run the 7.125.x build with that schema first on its search_path ` +
|
|
178
|
+
`(for example ALTER ROLE <role> SET search_path = <that schema>, <the rest>), then restore the search_path and start this build. ` +
|
|
179
|
+
`The check behind this refusal is read-only and runs before this build creates any table or rewrites anything: it changed nothing in the database.`);
|
|
180
|
+
this.found = found;
|
|
181
|
+
this.name = "RetiredSchemaPresentError";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export async function assertRetiredSchemaAbsent(read, dialect) {
|
|
185
|
+
const live = await readLiveColumns(read, dialect);
|
|
186
|
+
const found = [];
|
|
187
|
+
if (live.has(RETIRED_SNAPSHOT_TABLE))
|
|
188
|
+
found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
|
|
189
|
+
if (live.get(RETIRED_STEER_SEAT.table)?.has(RETIRED_STEER_SEAT.column) === true) {
|
|
190
|
+
found.push(`column ${RETIRED_STEER_SEAT.table}.${RETIRED_STEER_SEAT.column} (the retired single-seat steering column)`);
|
|
191
|
+
}
|
|
192
|
+
if (found.length > 0)
|
|
193
|
+
throw new RetiredSchemaPresentError(found);
|
|
194
|
+
}
|
|
195
|
+
export async function assertRetiredSchemaAbsentOnPool(target) {
|
|
196
|
+
if (target.dialect === "tidb") {
|
|
197
|
+
const conn = await target.pool.getConnection();
|
|
198
|
+
try {
|
|
199
|
+
await assertRetiredSchemaAbsent(async (sql) => (await conn.query(sql))[0], "mysql");
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
conn.release();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const client = await target.pool.connect();
|
|
207
|
+
try {
|
|
208
|
+
await assertRetiredSchemaAbsent(async (sql) => (await client.query(sql)).rows, "pg");
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
client.release();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
162
214
|
//# sourceMappingURL=schema-consistency.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One row of the scan: its `seq` and the `payload` column projected as text (`jsonColumnText`). */
|
|
2
|
+
export interface EpochAnchorRow {
|
|
3
|
+
readonly seq: string | number;
|
|
4
|
+
readonly payload: unknown;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Page the rows `seq < beforeSeq` whose type is `prompt_epoch` or `compaction`, newest first (the backend's own SQL;
|
|
8
|
+
* an empty page = no older row).
|
|
9
|
+
*/
|
|
10
|
+
export type EpochAnchorPage = (beforeSeq: number) => Promise<readonly EpochAnchorRow[]>;
|
|
11
|
+
/** The nearest epoch carrier below `floorSeq`, as stored; see the file header. */
|
|
12
|
+
export declare function scanEpochAnchor(sessionId: string, floorSeq: number, page: EpochAnchorPage): Promise<unknown>;
|
|
13
|
+
//# sourceMappingURL=session-epoch-anchor.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
2
|
+
export async function scanEpochAnchor(sessionId, floorSeq, page) {
|
|
3
|
+
if (floorSeq <= 0)
|
|
4
|
+
return undefined;
|
|
5
|
+
let cursor = floorSeq;
|
|
6
|
+
for (;;) {
|
|
7
|
+
const rows = await page(cursor);
|
|
8
|
+
if (rows.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
for (const row of rows) {
|
|
11
|
+
const entry = decodeSessionEventPayload(row.payload, sessionId, row.seq);
|
|
12
|
+
if (entry.type === "prompt_epoch")
|
|
13
|
+
return entry;
|
|
14
|
+
if (entry.type === "compaction") {
|
|
15
|
+
const details = entry.details;
|
|
16
|
+
if (typeof details === "object" && details !== null && "promptEpoch" in details && details.promptEpoch !== undefined)
|
|
17
|
+
return details.promptEpoch;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
cursor = Number(rows[rows.length - 1].seq);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=session-epoch-anchor.js.map
|
|
@@ -125,12 +125,18 @@ export interface SqlSharedMemoryStoreOptions {
|
|
|
125
125
|
* hope (core puts no cap on the provider's window, so the provider must state its own). */
|
|
126
126
|
maxListLimit?: number;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* WRITE-side store-id admission. The rule is core's own judge ({@link isSharedMemoryStoreId}): a row failing it would be dropped
|
|
130
|
+
* from every listing — an id the model cannot echo back is worse than an absent one — so we refuse to store it in the first place.
|
|
131
|
+
* This module keeps no copy of the pattern (nor of the `__` ban): the protocol table's namespace separator is core's to spell.
|
|
132
|
+
*/
|
|
128
133
|
export declare function assertStoreId(storeId: string): void;
|
|
129
134
|
export declare function assertScopeKey(scopeKey: string): void;
|
|
130
135
|
/**
|
|
131
|
-
* WRITE-side document-path admission
|
|
132
|
-
*
|
|
133
|
-
*
|
|
136
|
+
* WRITE-side document-path admission: core's own judge ({@link isSharedMemoryDocumentPath} — the SAME predicate core's listing
|
|
137
|
+
* gate applies, so a row core would silently drop is refused here where an operator can see it), plus the two rules that are
|
|
138
|
+
* stricter on purpose and are this store's own: the column width, and the non-BMP refusal — see the header's ORDERING section.
|
|
139
|
+
* This module keeps no copy of core's rule (forbidden characters, segment shape, suffix table).
|
|
134
140
|
*/
|
|
135
141
|
export declare function assertDocumentPath(path: string): void;
|
|
136
142
|
/**
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError, } from "@sema-agent/core";
|
|
1
|
+
import { isSharedMemoryDocumentPath, isSharedMemoryStoreId, SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError, } from "@sema-agent/core";
|
|
2
2
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
3
3
|
import { isDupKeyError } from "./sql-errors.js";
|
|
4
4
|
const isDupKey = isDupKeyError;
|
|
5
5
|
export const SHARED_MEMORY_SCOPE_TABLE = "shared_memory_scope";
|
|
6
6
|
export const SHARED_MEMORY_STORE_TABLE = "shared_memory_store";
|
|
7
7
|
export const SHARED_MEMORY_ENTRY_TABLE = "shared_memory_entry";
|
|
8
|
-
const STORE_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
9
8
|
const ORG_SCOPE_RE = /^org:\S{1,185}$/;
|
|
10
|
-
const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
|
|
11
|
-
const DOCUMENT_SUFFIXES = [".md", ".txt", ".json", ".jsonl"];
|
|
12
9
|
const PATH_MAX_CHARS = 512;
|
|
13
10
|
const DESCRIPTION_MAX_CHARS = 512;
|
|
14
11
|
const DEFAULT_MAX_LIST_LIMIT = 500;
|
|
15
12
|
export function assertStoreId(storeId) {
|
|
16
|
-
if (!
|
|
17
|
-
throw new Error(`shared-memory store id ${JSON.stringify(storeId)} is not usable:
|
|
13
|
+
if (!isSharedMemoryStoreId(storeId)) {
|
|
14
|
+
throw new Error(`shared-memory store id ${JSON.stringify(storeId)} is not usable: core's shared-memory store-id rule refuses it`);
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
17
|
export function assertScopeKey(scopeKey) {
|
|
@@ -26,18 +23,10 @@ export function assertDocumentPath(path) {
|
|
|
26
23
|
const refuse = (why) => {
|
|
27
24
|
throw new Error(`shared-memory document path ${JSON.stringify(path)} is not usable: ${why}`);
|
|
28
25
|
};
|
|
29
|
-
if (!path.startsWith("/"))
|
|
30
|
-
refuse("it must be absolute (leading \"/\")");
|
|
31
26
|
if (path.length > PATH_MAX_CHARS)
|
|
32
27
|
refuse(`it is ${path.length} UTF-16 units, over the ${PATH_MAX_CHARS} the column stores`);
|
|
33
|
-
if (
|
|
34
|
-
refuse("
|
|
35
|
-
const segments = path.slice(1).split("/");
|
|
36
|
-
if (!segments.every((s) => s !== "" && !s.startsWith(".")))
|
|
37
|
-
refuse("every folder segment must be non-empty and must not start with \".\"");
|
|
38
|
-
const last = segments.at(-1);
|
|
39
|
-
if (last === undefined || !DOCUMENT_SUFFIXES.some((s) => last.endsWith(s)))
|
|
40
|
-
refuse(`the filename must end in ${DOCUMENT_SUFFIXES.join(", ")}`);
|
|
28
|
+
if (!isSharedMemoryDocumentPath(path))
|
|
29
|
+
refuse("core's shared-memory document-path rule refuses it");
|
|
41
30
|
for (const ch of path) {
|
|
42
31
|
if (ch.codePointAt(0) > 0xffff)
|
|
43
32
|
refuse("it carries a supplementary-plane character, which would make SQL ordering and core's UTF-16 cursor ordering disagree");
|