@botlearn-course/daemon 0.0.16 → 0.0.17-beta.1
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 +3 -0
- package/dist/agent-service-sandbox.js +35 -16
- package/dist/input-attachments.d.ts +4 -2
- package/dist/input-attachments.js +34 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,9 @@ runtime 凭据。
|
|
|
102
102
|
- 文件扫描只访问 workspace 内路径;包含 `..`、绝对路径或反斜杠的路径会被拒绝。daemon
|
|
103
103
|
只上报相对路径、大小、sha256 和有界脱敏 preview,文件正文始终留在本地或托管 sandbox
|
|
104
104
|
的持久 workspace,不上传 Course Service 或对象存储。扫描或 metadata 上报失败不影响 run 终态。
|
|
105
|
+
- 托管 Practice Agent 的学员输入附件由 Course Service 为当前 activation 签发单对象、短时效的
|
|
106
|
+
存储源站 URL;daemon 直接下载并复核 grant 集合、大小、SHA-256、MIME 与图片签名。URL 只留在
|
|
107
|
+
control process 内存,不进入 runtime env、prompt、workspace state、transcript、事件或日志。
|
|
105
108
|
- 每个 run 在独立 workspace 内执行并有超时上限;runtime 进程在取消/超时后会被终止
|
|
106
109
|
(SIGTERM → SIGKILL)。
|
|
107
110
|
- `report_progress` MCP 子进程以 clean environment 启动,不继承 Course 控制字段或模型凭据;
|
|
@@ -15,6 +15,7 @@ const MAX_SPOOL_FRAMES = 1024;
|
|
|
15
15
|
const MAX_SPOOL_BYTES = 8 * 1024 * 1024;
|
|
16
16
|
const MAX_EVENT_ACK_WINDOW = 64;
|
|
17
17
|
const LEGACY_EVENT_ACK_WINDOW = 1;
|
|
18
|
+
const INPUT_ATTACHMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
18
19
|
/** 出站 seq 基址:seq = connection_epoch * SEQ_EPOCH_BASE + n,跨重连单调(合同 §1.1)。 */
|
|
19
20
|
const SEQ_EPOCH_BASE = 1_000_000_000;
|
|
20
21
|
const RUNTIME_LOG_WINDOW_MS = 60_000;
|
|
@@ -88,26 +89,44 @@ function inputAttachmentGrant(value) {
|
|
|
88
89
|
throw new Error("invalid_input_attachment_grant");
|
|
89
90
|
}
|
|
90
91
|
const candidate = value;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
if (!Array.isArray(candidate.downloads) ||
|
|
93
|
+
candidate.downloads.length < 1 ||
|
|
94
|
+
candidate.downloads.length > 5) {
|
|
94
95
|
throw new Error("invalid_input_attachment_grant");
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
parsed
|
|
97
|
+
const downloads = candidate.downloads.map((value) => {
|
|
98
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
99
|
+
throw new Error("invalid_input_attachment_grant");
|
|
100
|
+
}
|
|
101
|
+
const raw = value;
|
|
102
|
+
const attachmentId = typeof raw.attachment_id === "string"
|
|
103
|
+
? raw.attachment_id.trim()
|
|
104
|
+
: "";
|
|
105
|
+
const downloadUrl = typeof raw.download_url === "string"
|
|
106
|
+
? raw.download_url.trim()
|
|
107
|
+
: "";
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = new URL(downloadUrl);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error("invalid_input_attachment_grant");
|
|
114
|
+
}
|
|
115
|
+
if (!INPUT_ATTACHMENT_ID.test(attachmentId) ||
|
|
116
|
+
!downloadUrl ||
|
|
117
|
+
downloadUrl.length > 4096 ||
|
|
118
|
+
!["http:", "https:"].includes(parsed.protocol) ||
|
|
119
|
+
parsed.username ||
|
|
120
|
+
parsed.password ||
|
|
121
|
+
parsed.hash) {
|
|
122
|
+
throw new Error("invalid_input_attachment_grant");
|
|
123
|
+
}
|
|
124
|
+
return { attachmentId, downloadUrl };
|
|
125
|
+
});
|
|
126
|
+
if (new Set(downloads.map((item) => item.attachmentId)).size !== downloads.length) {
|
|
108
127
|
throw new Error("invalid_input_attachment_grant");
|
|
109
128
|
}
|
|
110
|
-
return {
|
|
129
|
+
return { downloads };
|
|
111
130
|
}
|
|
112
131
|
function runtimeSkillGrantKey(grants) {
|
|
113
132
|
return JSON.stringify(grants);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { MaterializedInputAttachment, RunInputAttachment } from "./types.js";
|
|
2
2
|
export interface InputAttachmentGrant {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
downloads: Array<{
|
|
4
|
+
attachmentId: string;
|
|
5
|
+
downloadUrl: string;
|
|
6
|
+
}>;
|
|
5
7
|
}
|
|
6
8
|
export declare function materializeInputAttachments(workspaceDir: string, attachments: RunInputAttachment[], grant: InputAttachmentGrant, signal: AbortSignal): Promise<MaterializedInputAttachment[]>;
|
|
@@ -58,26 +58,39 @@ function validateAttachment(attachment) {
|
|
|
58
58
|
}
|
|
59
59
|
return extension;
|
|
60
60
|
}
|
|
61
|
-
function
|
|
62
|
-
if (!
|
|
63
|
-
throw new Error("invalid input attachment download
|
|
61
|
+
function validateDownloadUrl(value) {
|
|
62
|
+
if (!value || value.length > 4096) {
|
|
63
|
+
throw new Error("invalid input attachment download URL");
|
|
64
64
|
}
|
|
65
|
-
let
|
|
65
|
+
let parsed;
|
|
66
66
|
try {
|
|
67
|
-
|
|
67
|
+
parsed = new URL(value);
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
70
|
throw new Error("invalid input attachment download URL");
|
|
71
71
|
}
|
|
72
|
-
if (!["http:", "https:"].includes(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
base.hash ||
|
|
77
|
-
base.toString().length > 2048) {
|
|
72
|
+
if (!["http:", "https:"].includes(parsed.protocol) ||
|
|
73
|
+
parsed.username ||
|
|
74
|
+
parsed.password ||
|
|
75
|
+
parsed.hash) {
|
|
78
76
|
throw new Error("invalid input attachment download URL");
|
|
79
77
|
}
|
|
80
|
-
return
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function validateGrant(grant) {
|
|
81
|
+
if (!Array.isArray(grant.downloads) ||
|
|
82
|
+
grant.downloads.length < 1 ||
|
|
83
|
+
grant.downloads.length > MAX_ATTACHMENTS) {
|
|
84
|
+
throw new Error("invalid input attachment download grant");
|
|
85
|
+
}
|
|
86
|
+
const downloads = new Map();
|
|
87
|
+
for (const item of grant.downloads) {
|
|
88
|
+
if (!ATTACHMENT_ID.test(item.attachmentId) || downloads.has(item.attachmentId)) {
|
|
89
|
+
throw new Error("invalid input attachment download grant");
|
|
90
|
+
}
|
|
91
|
+
downloads.set(item.attachmentId, validateDownloadUrl(item.downloadUrl));
|
|
92
|
+
}
|
|
93
|
+
return downloads;
|
|
81
94
|
}
|
|
82
95
|
function hasExpectedImageSignature(contentType, bytes) {
|
|
83
96
|
switch (contentType) {
|
|
@@ -177,7 +190,12 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
177
190
|
if (new Set(attachments.map((item) => item.attachment_id)).size !== attachments.length) {
|
|
178
191
|
throw new Error("duplicate input attachment id");
|
|
179
192
|
}
|
|
180
|
-
const
|
|
193
|
+
const extensions = new Map(attachments.map((attachment) => [attachment.attachment_id, validateAttachment(attachment)]));
|
|
194
|
+
const downloadUrls = validateGrant(grant);
|
|
195
|
+
if (downloadUrls.size !== attachments.length ||
|
|
196
|
+
attachments.some((attachment) => !downloadUrls.has(attachment.attachment_id))) {
|
|
197
|
+
throw new Error("input attachment download grant does not match run attachments");
|
|
198
|
+
}
|
|
181
199
|
const inputDir = path.resolve(workspaceDir, ".botlearn", "input-attachments");
|
|
182
200
|
const workspaceRoot = path.resolve(workspaceDir);
|
|
183
201
|
if (!inputDir.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
@@ -188,7 +206,7 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
188
206
|
await chmod(inputDir, 0o770);
|
|
189
207
|
const results = [];
|
|
190
208
|
for (const attachment of attachments) {
|
|
191
|
-
const extension =
|
|
209
|
+
const extension = extensions.get(attachment.attachment_id);
|
|
192
210
|
const filename = `${attachment.attachment_id}${extension}`;
|
|
193
211
|
const filePath = path.join(inputDir, filename);
|
|
194
212
|
const relativePath = path.posix.join(".botlearn", "input-attachments", filename);
|
|
@@ -196,9 +214,9 @@ export async function materializeInputAttachments(workspaceDir, attachments, gra
|
|
|
196
214
|
await rm(filePath, { force: true });
|
|
197
215
|
const partPath = `${filePath}.part`;
|
|
198
216
|
await rm(partPath, { force: true });
|
|
199
|
-
const downloadUrl =
|
|
217
|
+
const downloadUrl = downloadUrls.get(attachment.attachment_id);
|
|
200
218
|
const response = await fetch(downloadUrl, {
|
|
201
|
-
headers: {
|
|
219
|
+
headers: { "accept-encoding": "identity" },
|
|
202
220
|
redirect: "error",
|
|
203
221
|
signal,
|
|
204
222
|
});
|
package/package.json
CHANGED