@botlearn-course/daemon 0.0.20-beta.1 → 0.0.20-beta.10
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 +7 -0
- package/dist/agent-service-sandbox.d.ts +7 -1
- package/dist/agent-service-sandbox.js +417 -39
- package/dist/agent-service-ws-protocol.d.ts +3 -3
- package/dist/agent-service-ws-protocol.js +5 -2
- package/dist/cli.js +19 -1
- package/dist/file-candidates.js +46 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-dispatcher.d.ts +1 -1
- package/dist/run-dispatcher.js +10 -7
- package/dist/runtime-env.js +4 -4
- package/dist/runtime-quiescence.d.ts +19 -0
- package/dist/runtime-quiescence.js +130 -0
- package/dist/runtimes/deepseek-tui.js +11 -0
- package/dist/tool-observation.d.ts +6 -5
- package/dist/tool-observation.js +6 -5
- package/dist/trace-projection.d.ts +1 -1
- package/dist/trace-projection.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/workspace-filesystem.d.ts +20 -0
- package/dist/workspace-filesystem.js +120 -0
- package/dist/workspace-quota.d.ts +4 -0
- package/dist/workspace-quota.js +42 -0
- package/dist/workspace-restore.d.ts +9 -1
- package/dist/workspace-restore.js +30 -16
- package/dist/workspace-snapshot-control.d.ts +29 -0
- package/dist/workspace-snapshot-control.js +169 -0
- package/dist/workspace-snapshot-policy.d.ts +24 -0
- package/dist/workspace-snapshot-policy.js +45 -0
- package/dist/workspace-snapshot-staging.d.ts +2 -9
- package/dist/workspace-snapshot-staging.js +59 -25
- package/dist/workspace.d.ts +4 -4
- package/dist/workspace.js +26 -11
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
|
|
2
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isWorkspaceWriterFreezeProof, } from "./runtime-quiescence.js";
|
|
4
5
|
import { defaultCaseFold, validateWorkspaceEntrySet, WORKSPACE_ENTRY_SET_SCHEMA, } from "./workspace-entry-set.js";
|
|
5
6
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
7
|
const EXCLUDED_NAMES = new Set(["node_modules"]);
|
|
@@ -107,30 +108,52 @@ function fsyncDirectory(directory) {
|
|
|
107
108
|
closeSync(handle);
|
|
108
109
|
}
|
|
109
110
|
}
|
|
111
|
+
function descriptorPath(handle, fallback) {
|
|
112
|
+
if (existsSync("/proc/self/fd")) {
|
|
113
|
+
const value = path.join("/proc/self/fd", String(handle));
|
|
114
|
+
if (!existsSync(value))
|
|
115
|
+
fail("workspace_snapshot_fd_traversal_unavailable");
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
// Darwin has no procfs open-file path that supports relative child lookup. It is used
|
|
119
|
+
// only by local tests/BYOA; managed durable sandboxes are Linux and fail closed above.
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
110
122
|
function stageOnce(options, paths) {
|
|
111
123
|
const stagingDirectory = path.join(paths.checkpointRoot, randomUUID());
|
|
112
124
|
mkdirSync(stagingDirectory, { mode: 0o700 });
|
|
113
125
|
chmodSync(stagingDirectory, 0o700);
|
|
114
126
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
127
|
+
const rootHandle = openSync(paths.workspace, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
128
|
+
const rootStats = fstatSync(rootHandle, { bigint: true });
|
|
129
|
+
if (!rootStats.isDirectory()) {
|
|
130
|
+
closeSync(rootHandle);
|
|
117
131
|
fail("workspace_snapshot_root_invalid");
|
|
132
|
+
}
|
|
118
133
|
const entries = [];
|
|
119
134
|
const files = [];
|
|
120
135
|
let fileCount = 0;
|
|
121
136
|
let totalBytes = 0;
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
let estimatedEntrySetBytes = Buffer.byteLength('{"schemaVersion":"agent-workspace-entry-set/1","entries":[]}', "utf8");
|
|
138
|
+
function appendEntry(entry) {
|
|
139
|
+
estimatedEntrySetBytes += Buffer.byteLength(JSON.stringify(entry), "utf8") + 1;
|
|
140
|
+
if (estimatedEntrySetBytes > options.limits.maxEntrySetBytes) {
|
|
141
|
+
fail("workspace_snapshot_limit_exceeded");
|
|
142
|
+
}
|
|
143
|
+
entries.push(entry);
|
|
144
|
+
}
|
|
145
|
+
function walk(sourceHandle, sourceFallback, destinationDirectory, segments) {
|
|
146
|
+
const directoryBefore = fstatSync(sourceHandle, { bigint: true });
|
|
124
147
|
assertSourceNode(directoryBefore, {
|
|
125
148
|
rootDevice: rootStats.dev,
|
|
126
149
|
expected: "directory",
|
|
127
150
|
});
|
|
151
|
+
const sourceDirectory = descriptorPath(sourceHandle, sourceFallback);
|
|
128
152
|
const children = readdirSync(sourceDirectory, {
|
|
129
|
-
withFileTypes: true,
|
|
130
153
|
encoding: "buffer",
|
|
131
|
-
}).sort((left, right) => Buffer.compare(left
|
|
154
|
+
}).sort((left, right) => Buffer.compare(left, right));
|
|
132
155
|
for (const child of children) {
|
|
133
|
-
const name = decodeName(child
|
|
156
|
+
const name = decodeName(child);
|
|
134
157
|
if (excludedName(name))
|
|
135
158
|
continue;
|
|
136
159
|
const relativeSegments = [...segments, name];
|
|
@@ -145,13 +168,21 @@ function stageOnce(options, paths) {
|
|
|
145
168
|
fail("workspace_snapshot_unsupported_file_type");
|
|
146
169
|
if (stats.isDirectory()) {
|
|
147
170
|
assertSourceNode(stats, { rootDevice: rootStats.dev, expected: "directory" });
|
|
171
|
+
const childHandle = openSync(source, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
172
|
+
const openedDirectory = fstatSync(childHandle, { bigint: true });
|
|
173
|
+
if (!sameIdentity(stats, openedDirectory)) {
|
|
174
|
+
closeSync(childHandle);
|
|
175
|
+
fail("workspace_snapshot_source_changed", true);
|
|
176
|
+
}
|
|
148
177
|
mkdirSync(destination, { mode: 0o700 });
|
|
149
178
|
chmodSync(destination, 0o700);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
appendEntry({ type: "directory", path: relativePath, mode: "0700" });
|
|
180
|
+
try {
|
|
181
|
+
walk(childHandle, source, destination, relativeSegments);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
closeSync(childHandle);
|
|
153
185
|
}
|
|
154
|
-
walk(source, destination, relativeSegments);
|
|
155
186
|
continue;
|
|
156
187
|
}
|
|
157
188
|
const copied = copyStableFile(source, destination, {
|
|
@@ -163,7 +194,7 @@ function stageOnce(options, paths) {
|
|
|
163
194
|
if (fileCount > options.limits.maxFileCount ||
|
|
164
195
|
totalBytes > options.limits.maxTotalBytes)
|
|
165
196
|
fail("workspace_snapshot_limit_exceeded");
|
|
166
|
-
|
|
197
|
+
appendEntry({
|
|
167
198
|
type: "file",
|
|
168
199
|
path: relativePath,
|
|
169
200
|
mode: copied.mode,
|
|
@@ -176,17 +207,19 @@ function stageOnce(options, paths) {
|
|
|
176
207
|
sizeBytes: copied.sizeBytes,
|
|
177
208
|
sha256: copied.sha256,
|
|
178
209
|
});
|
|
179
|
-
if (entries.length > options.limits.maxEntrySetBytes) {
|
|
180
|
-
fail("workspace_snapshot_limit_exceeded");
|
|
181
|
-
}
|
|
182
210
|
}
|
|
183
|
-
const directoryAfter =
|
|
211
|
+
const directoryAfter = fstatSync(sourceHandle, { bigint: true });
|
|
184
212
|
if (!sameIdentity(directoryBefore, directoryAfter)) {
|
|
185
213
|
fail("workspace_snapshot_source_changed", true);
|
|
186
214
|
}
|
|
187
215
|
fsyncDirectory(destinationDirectory);
|
|
188
216
|
}
|
|
189
|
-
|
|
217
|
+
try {
|
|
218
|
+
walk(rootHandle, paths.workspace, stagingDirectory, []);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
closeSync(rootHandle);
|
|
222
|
+
}
|
|
190
223
|
entries.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
|
|
191
224
|
files.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
|
|
192
225
|
const entrySet = validateWorkspaceEntrySet({ schemaVersion: WORKSPACE_ENTRY_SET_SCHEMA, entries }, options.limits);
|
|
@@ -200,13 +233,14 @@ function stageOnce(options, paths) {
|
|
|
200
233
|
export function stageWorkspaceSnapshot(options) {
|
|
201
234
|
if (!UUID.test(options.checkpointId))
|
|
202
235
|
fail("workspace_snapshot_checkpoint_invalid");
|
|
203
|
-
if (options.
|
|
204
|
-
|
|
205
|
-
!UUID.test(options.
|
|
206
|
-
options.
|
|
207
|
-
|
|
208
|
-
options.
|
|
209
|
-
options.
|
|
236
|
+
if (!isWorkspaceWriterFreezeProof(options.freezeProof) ||
|
|
237
|
+
options.freezeProof.proofVersion !== "workspace-writer-freeze-proof/1" ||
|
|
238
|
+
!UUID.test(options.freezeProof.sandboxId) ||
|
|
239
|
+
!UUID.test(options.freezeProof.runtimeSessionId) ||
|
|
240
|
+
options.freezeProof.checkpointId !== options.checkpointId ||
|
|
241
|
+
!Number.isSafeInteger(options.freezeProof.sandboxGeneration) ||
|
|
242
|
+
options.freezeProof.sandboxGeneration < 1 ||
|
|
243
|
+
options.freezeProof.allWritersFrozen !== true)
|
|
210
244
|
fail("workspace_snapshot_writer_isolation_unproven");
|
|
211
245
|
if (!Number.isSafeInteger(options.maxStabilityRetries) || options.maxStabilityRetries < 0) {
|
|
212
246
|
fail("workspace_snapshot_retry_limit_invalid");
|
package/dist/workspace.d.ts
CHANGED
|
@@ -20,9 +20,9 @@ export declare function runtimeProfileDir(agentRunId: string): string;
|
|
|
20
20
|
export declare function runtimeProfileRunRootDir(agentRunId: string): string;
|
|
21
21
|
export declare function runtimeSessionRootDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
22
22
|
/**
|
|
23
|
-
* per-session workspace(ADR-015 §7):managed sandbox
|
|
24
|
-
* `<
|
|
25
|
-
*
|
|
23
|
+
* per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
|
|
24
|
+
* `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
|
|
25
|
+
* 两种路径都由 daemon 在 session.open 时创建。
|
|
26
26
|
*/
|
|
27
27
|
export declare function runtimeSessionWorkspaceDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
28
28
|
export declare function runtimeSessionTranscriptPath(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): string;
|
|
@@ -102,7 +102,7 @@ export declare function exposeRuntimeSessionWorkspace(runtimeSessionId: string,
|
|
|
102
102
|
/** Revoke a managed workspace without deleting the session's durable local materialization. */
|
|
103
103
|
export declare function revokeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
104
104
|
/** session.close 时删除 workspace 与本地 session 物化状态(transcripts 等)。幂等。 */
|
|
105
|
-
export declare function removeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
105
|
+
export declare function removeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number, preserveManagedWorkspace?: boolean): void;
|
|
106
106
|
export declare function ensureRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): {
|
|
107
107
|
rootDir: string;
|
|
108
108
|
workspaceDir: string;
|
package/dist/workspace.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { TextDecoder } from "node:util";
|
|
6
6
|
import { daemonHome } from "./auth-store.js";
|
|
7
|
+
import { nasWorkspaceEnabled } from "./workspace-filesystem.js";
|
|
7
8
|
/**
|
|
8
9
|
* 每 run 隔离工作区(spec §4):
|
|
9
10
|
*
|
|
@@ -53,9 +54,9 @@ export function runtimeSessionRootDir(runtimeSessionId, sandboxGeneration) {
|
|
|
53
54
|
return path.join(daemonHome(), "agent-service-sessions", runtimeSessionId, `generation-${sandboxGeneration}`);
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
* per-session workspace(ADR-015 §7):managed sandbox
|
|
57
|
-
* `<
|
|
58
|
-
*
|
|
57
|
+
* per-session workspace(ADR-015 §7):NAS managed sandbox 使用稳定路径
|
|
58
|
+
* `<root>/sessions/<runtime_session_id>/workspace`;旧 provider 保留 generation 路径。
|
|
59
|
+
* 两种路径都由 daemon 在 session.open 时创建。
|
|
59
60
|
*/
|
|
60
61
|
export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration) {
|
|
61
62
|
assertSafeId(runtimeSessionId, "runtime_session_id");
|
|
@@ -64,6 +65,9 @@ export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration)
|
|
|
64
65
|
}
|
|
65
66
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
66
67
|
if (managedRoot) {
|
|
68
|
+
if (nasWorkspaceEnabled()) {
|
|
69
|
+
return path.join(managedRoot, "sessions", runtimeSessionId, "workspace");
|
|
70
|
+
}
|
|
67
71
|
return path.join(managedRoot, runtimeSessionId, `generation-${sandboxGeneration}`);
|
|
68
72
|
}
|
|
69
73
|
return path.join(runtimeSessionRootDir(runtimeSessionId, sandboxGeneration), "workspace");
|
|
@@ -71,7 +75,11 @@ export function runtimeSessionWorkspaceDir(runtimeSessionId, sandboxGeneration)
|
|
|
71
75
|
function managedRuntimeSessionParentDir(runtimeSessionId) {
|
|
72
76
|
assertSafeId(runtimeSessionId, "runtime_session_id");
|
|
73
77
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
74
|
-
|
|
78
|
+
if (!managedRoot)
|
|
79
|
+
return null;
|
|
80
|
+
return nasWorkspaceEnabled()
|
|
81
|
+
? path.join(managedRoot, "sessions", runtimeSessionId)
|
|
82
|
+
: path.join(managedRoot, runtimeSessionId);
|
|
75
83
|
}
|
|
76
84
|
export function runtimeSessionTranscriptPath(runtimeSessionId, sandboxGeneration, agentRunId) {
|
|
77
85
|
assertSafeId(agentRunId, "agent_run_id");
|
|
@@ -228,12 +236,16 @@ async function childDirectories(root) {
|
|
|
228
236
|
export async function cleanupRuntimeSessionWorkspaceCopyStaging() {
|
|
229
237
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
230
238
|
if (managedRoot) {
|
|
231
|
-
|
|
239
|
+
const sessionRoot = nasWorkspaceEnabled()
|
|
240
|
+
? path.join(managedRoot, "sessions")
|
|
241
|
+
: managedRoot;
|
|
242
|
+
for (const sessionName of await childDirectories(sessionRoot)) {
|
|
232
243
|
if (!SAFE_ID_PATTERN.test(sessionName))
|
|
233
244
|
continue;
|
|
234
|
-
const sessionParent = path.join(
|
|
245
|
+
const sessionParent = path.join(sessionRoot, sessionName);
|
|
235
246
|
for (const name of await childDirectories(sessionParent)) {
|
|
236
|
-
if (MANAGED_COPY_STAGING_PATTERN.test(name)
|
|
247
|
+
if (MANAGED_COPY_STAGING_PATTERN.test(name)
|
|
248
|
+
|| LOCAL_COPY_STAGING_PATTERN.test(name)) {
|
|
237
249
|
await fs.rm(path.join(sessionParent, name), { recursive: true, force: true });
|
|
238
250
|
}
|
|
239
251
|
}
|
|
@@ -700,16 +712,19 @@ export function revokeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneratio
|
|
|
700
712
|
}
|
|
701
713
|
}
|
|
702
714
|
/** session.close 时删除 workspace 与本地 session 物化状态(transcripts 等)。幂等。 */
|
|
703
|
-
export function removeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration) {
|
|
704
|
-
//
|
|
715
|
+
export function removeRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration, preserveManagedWorkspace = false) {
|
|
716
|
+
// 校验路径段安全后删除本地物化状态;generation 换代可保留 NAS 正文。
|
|
705
717
|
runtimeSessionRootDir(runtimeSessionId, sandboxGeneration);
|
|
706
718
|
rmSync(path.join(daemonHome(), "agent-service-sessions", runtimeSessionId), {
|
|
707
719
|
recursive: true,
|
|
708
720
|
force: true,
|
|
709
721
|
});
|
|
710
722
|
const managedRoot = process.env.BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT?.trim();
|
|
711
|
-
if (managedRoot) {
|
|
712
|
-
|
|
723
|
+
if (managedRoot && !preserveManagedWorkspace) {
|
|
724
|
+
const sessionRoot = nasWorkspaceEnabled()
|
|
725
|
+
? path.join(managedRoot, "sessions", runtimeSessionId)
|
|
726
|
+
: path.join(managedRoot, runtimeSessionId);
|
|
727
|
+
rmSync(sessionRoot, { recursive: true, force: true });
|
|
713
728
|
}
|
|
714
729
|
}
|
|
715
730
|
export function ensureRuntimeSessionWorkspace(runtimeSessionId, sandboxGeneration, agentRunId) {
|
package/package.json
CHANGED