@downcity/agent 1.1.205 → 1.1.207
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/bin/session/SessionTitle.d.ts.map +1 -1
- package/bin/session/SessionTitle.js +13 -5
- package/bin/session/SessionTitle.js.map +1 -1
- package/package.json +3 -3
- package/scripts/agent-env-shell-sandbox.test.mjs +105 -0
- package/scripts/agent-ready.test.mjs +1 -2
- package/scripts/session-title-event.test.mjs +49 -24
- package/scripts/shell-sandbox-env.test.mjs +18 -0
- package/src/session/SessionTitle.ts +13 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionTitle.d.ts","sourceRoot":"","sources":["../../src/session/SessionTitle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"SessionTitle.d.ts","sourceRoot":"","sources":["../../src/session/SessionTitle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,IAAI,CAAC;AAEpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AASvD;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,EAAE,eAAe,EAAE,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA+LD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA2C/B"}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - title 默认允许为空;只有模型成功生成标题时才会写入。
|
|
7
7
|
* - 当 title 仍为空时,后续执行链路可以再次尝试生成。
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { streamText } from "ai";
|
|
10
|
+
import { buildOpenAIResponsesProviderOptions } from "../executor/messages/SessionMessageCodec.js";
|
|
10
11
|
import { is_session_message_record } from "../executor/types/SessionRecords.js";
|
|
11
12
|
import { normalizeSessionTitle, readSessionMetadata, writeSessionMetadata, } from "../session/storage/Metadata.js";
|
|
12
13
|
const GENERATED_SESSION_TITLE_MAX_CHARS = 24;
|
|
@@ -86,8 +87,9 @@ async function logSessionTitleDiagnostic(input) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
async function generateSessionTitle(input) {
|
|
90
|
+
let observedStreamError;
|
|
89
91
|
try {
|
|
90
|
-
const result =
|
|
92
|
+
const result = streamText({
|
|
91
93
|
model: input.model,
|
|
92
94
|
system: "You generate minimal conversation titles. Output only the title itself, with no explanation and no quotation marks.",
|
|
93
95
|
prompt: [
|
|
@@ -96,8 +98,13 @@ async function generateSessionTitle(input) {
|
|
|
96
98
|
"",
|
|
97
99
|
input.firstUserText,
|
|
98
100
|
].join("\n"),
|
|
101
|
+
providerOptions: buildOpenAIResponsesProviderOptions(),
|
|
102
|
+
onError: ({ error }) => {
|
|
103
|
+
observedStreamError = error;
|
|
104
|
+
},
|
|
99
105
|
});
|
|
100
|
-
const
|
|
106
|
+
const text = await result.text;
|
|
107
|
+
const generatedTitle = normalizeGeneratedTitle(text);
|
|
101
108
|
if (!generatedTitle) {
|
|
102
109
|
await logSessionTitleDiagnostic({
|
|
103
110
|
logger: input.logger,
|
|
@@ -107,13 +114,14 @@ async function generateSessionTitle(input) {
|
|
|
107
114
|
details: {
|
|
108
115
|
modelLabel: input.modelLabel || null,
|
|
109
116
|
firstUserTextLength: input.firstUserText.length,
|
|
110
|
-
rawTitleLength: String(
|
|
117
|
+
rawTitleLength: String(text || "").length,
|
|
111
118
|
},
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
return generatedTitle;
|
|
115
122
|
}
|
|
116
123
|
catch (error) {
|
|
124
|
+
const effectiveError = observedStreamError || error;
|
|
117
125
|
await logSessionTitleDiagnostic({
|
|
118
126
|
logger: input.logger,
|
|
119
127
|
sessionId: input.sessionId,
|
|
@@ -122,7 +130,7 @@ async function generateSessionTitle(input) {
|
|
|
122
130
|
details: {
|
|
123
131
|
modelLabel: input.modelLabel || null,
|
|
124
132
|
firstUserTextLength: input.firstUserText.length,
|
|
125
|
-
...summarizeTitleError(
|
|
133
|
+
...summarizeTitleError(effectiveError),
|
|
126
134
|
},
|
|
127
135
|
});
|
|
128
136
|
// 关键点(中文):标题生成失败不能影响 session 主流程。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionTitle.js","sourceRoot":"","sources":["../../src/session/SessionTitle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"SessionTitle.js","sourceRoot":"","sources":["../../src/session/SessionTitle.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@downcity/agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.207",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
|
|
6
6
|
"main": "./bin/index.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@ai-sdk/openai-compatible": "^2.0.48",
|
|
20
|
-
"@downcity/shell": "^0.1.
|
|
21
|
-
"@downcity/type": "0.1.
|
|
20
|
+
"@downcity/shell": "^0.1.72",
|
|
21
|
+
"@downcity/type": "0.1.105",
|
|
22
22
|
"ai": "^6.0.193",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"dotenv": "^17.4.2",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 Agent 动态 env 会进入 shell safe sandbox。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 测试编译后的 package 入口,确保 SDK 用户拿到的行为与源码一致。
|
|
6
|
+
* - 只允许 Agent env 中显式存在的 key 进入 safe sandbox,不继承完整宿主环境。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
import { Agent } from "@downcity/agent";
|
|
16
|
+
import { Shell } from "@downcity/shell";
|
|
17
|
+
import {
|
|
18
|
+
checkShellSandboxPreflight,
|
|
19
|
+
} from "@downcity/shell/sandbox/SandboxPreflight.js";
|
|
20
|
+
|
|
21
|
+
async function execute_shell(agent, cmd) {
|
|
22
|
+
const result = await agent.tools.shell_exec.execute(
|
|
23
|
+
{
|
|
24
|
+
cmd,
|
|
25
|
+
shell: "/bin/sh",
|
|
26
|
+
login: false,
|
|
27
|
+
timeout_ms: 5000,
|
|
28
|
+
sandbox: "safe",
|
|
29
|
+
},
|
|
30
|
+
{ toolCallId: "agent-env-shell-sandbox-test" },
|
|
31
|
+
);
|
|
32
|
+
assert.equal(result.success, true, result.error || result.output);
|
|
33
|
+
return String(result.output || "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("agent setEnv and patchEnv are visible in shell safe sandbox", async (t) => {
|
|
37
|
+
const preflight = await checkShellSandboxPreflight();
|
|
38
|
+
if (!preflight.ok) {
|
|
39
|
+
t.skip(`safe sandbox unavailable: ${preflight.issues.map((issue) => issue.message).join("; ")}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const root_path = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-agent-env-"));
|
|
44
|
+
const previous_host_value = process.env.HOST_ONLY_ENV_REPRO;
|
|
45
|
+
process.env.HOST_ONLY_ENV_REPRO = "host_secret";
|
|
46
|
+
|
|
47
|
+
const agent = new Agent({
|
|
48
|
+
id: "agent-env-shell-sandbox-test",
|
|
49
|
+
path: root_path,
|
|
50
|
+
shell: new Shell(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
agent.setEnv({
|
|
55
|
+
DYNAMIC_ENV_REPRO: "initial_value",
|
|
56
|
+
DC_DYNAMIC_REPRO: "dc_initial",
|
|
57
|
+
REMOVED_ENV_REPRO: "remove_me",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const first_output = await execute_shell(
|
|
61
|
+
agent,
|
|
62
|
+
'printf "CURRENT=%s\\nDC=%s\\nREMOVED=%s\\nHOST_ONLY=%s\\n" "$DYNAMIC_ENV_REPRO" "$DC_DYNAMIC_REPRO" "$REMOVED_ENV_REPRO" "$HOST_ONLY_ENV_REPRO"',
|
|
63
|
+
);
|
|
64
|
+
assert.match(first_output, /CURRENT=initial_value/);
|
|
65
|
+
assert.match(first_output, /DC=dc_initial/);
|
|
66
|
+
assert.match(first_output, /REMOVED=remove_me/);
|
|
67
|
+
assert.match(first_output, /HOST_ONLY=\n/);
|
|
68
|
+
|
|
69
|
+
agent.getShell()?.configure({
|
|
70
|
+
env: {
|
|
71
|
+
DYNAMIC_ENV_REPRO: "configured_value",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const configured_output = await execute_shell(
|
|
76
|
+
agent,
|
|
77
|
+
'printf "CURRENT=%s\\nHOST_ONLY=%s\\n" "$DYNAMIC_ENV_REPRO" "$HOST_ONLY_ENV_REPRO"',
|
|
78
|
+
);
|
|
79
|
+
assert.match(configured_output, /CURRENT=configured_value/);
|
|
80
|
+
assert.match(configured_output, /HOST_ONLY=\n/);
|
|
81
|
+
|
|
82
|
+
agent.patchEnv({
|
|
83
|
+
DYNAMIC_ENV_REPRO: "updated_value",
|
|
84
|
+
ADDED_ENV_REPRO: "added_value",
|
|
85
|
+
REMOVED_ENV_REPRO: null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const second_output = await execute_shell(
|
|
89
|
+
agent,
|
|
90
|
+
'printf "CURRENT=%s\\nADDED=%s\\nREMOVED=%s\\nHOST_ONLY=%s\\n" "$DYNAMIC_ENV_REPRO" "$ADDED_ENV_REPRO" "$REMOVED_ENV_REPRO" "$HOST_ONLY_ENV_REPRO"',
|
|
91
|
+
);
|
|
92
|
+
assert.match(second_output, /CURRENT=updated_value/);
|
|
93
|
+
assert.match(second_output, /ADDED=added_value/);
|
|
94
|
+
assert.match(second_output, /REMOVED=\n/);
|
|
95
|
+
assert.match(second_output, /HOST_ONLY=\n/);
|
|
96
|
+
} finally {
|
|
97
|
+
await agent.dispose();
|
|
98
|
+
await fs.rm(root_path, { recursive: true, force: true });
|
|
99
|
+
if (previous_host_value === undefined) {
|
|
100
|
+
delete process.env.HOST_ONLY_ENV_REPRO;
|
|
101
|
+
} else {
|
|
102
|
+
process.env.HOST_ONLY_ENV_REPRO = previous_host_value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -159,10 +159,9 @@ test("session.prompt waits for agent background ready before model execution", a
|
|
|
159
159
|
const result = await turn.finished;
|
|
160
160
|
|
|
161
161
|
assert.equal(result.success, true);
|
|
162
|
-
assert.equal(model_stream_calls,
|
|
162
|
+
assert.equal(model_stream_calls, 2);
|
|
163
163
|
} finally {
|
|
164
164
|
lifecycle_ready.resolve();
|
|
165
165
|
await agent.dispose();
|
|
166
166
|
}
|
|
167
167
|
});
|
|
168
|
-
|
|
@@ -16,39 +16,64 @@ import fs from "node:fs/promises";
|
|
|
16
16
|
import { MockLanguageModelV3 } from "ai/test";
|
|
17
17
|
import { Agent } from "../bin/index.js";
|
|
18
18
|
|
|
19
|
+
function create_stream_text_result(text) {
|
|
20
|
+
return {
|
|
21
|
+
stream: new ReadableStream({
|
|
22
|
+
start(controller) {
|
|
23
|
+
controller.enqueue({
|
|
24
|
+
type: "stream-start",
|
|
25
|
+
warnings: [],
|
|
26
|
+
});
|
|
27
|
+
controller.enqueue({
|
|
28
|
+
type: "text-start",
|
|
29
|
+
id: "text_1",
|
|
30
|
+
});
|
|
31
|
+
controller.enqueue({
|
|
32
|
+
type: "text-delta",
|
|
33
|
+
id: "text_1",
|
|
34
|
+
delta: text,
|
|
35
|
+
});
|
|
36
|
+
controller.enqueue({
|
|
37
|
+
type: "text-end",
|
|
38
|
+
id: "text_1",
|
|
39
|
+
});
|
|
40
|
+
controller.enqueue({
|
|
41
|
+
type: "finish",
|
|
42
|
+
finishReason: {
|
|
43
|
+
unified: "stop",
|
|
44
|
+
raw: "stop",
|
|
45
|
+
},
|
|
46
|
+
usage: {
|
|
47
|
+
inputTokens: {
|
|
48
|
+
total: 0,
|
|
49
|
+
noCache: 0,
|
|
50
|
+
cacheRead: 0,
|
|
51
|
+
cacheWrite: 0,
|
|
52
|
+
},
|
|
53
|
+
outputTokens: {
|
|
54
|
+
total: 0,
|
|
55
|
+
text: 0,
|
|
56
|
+
reasoning: 0,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
controller.close();
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
19
66
|
function create_mock_title_model(title_text) {
|
|
20
67
|
return new MockLanguageModelV3({
|
|
21
68
|
modelId: "mock-session-title-model",
|
|
22
|
-
|
|
23
|
-
content: [
|
|
24
|
-
{
|
|
25
|
-
type: "text",
|
|
26
|
-
text: title_text,
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
finishReason: "stop",
|
|
30
|
-
usage: {
|
|
31
|
-
inputTokens: {
|
|
32
|
-
total: 0,
|
|
33
|
-
noCache: 0,
|
|
34
|
-
cacheRead: 0,
|
|
35
|
-
cacheWrite: 0,
|
|
36
|
-
},
|
|
37
|
-
outputTokens: {
|
|
38
|
-
total: 0,
|
|
39
|
-
text: 0,
|
|
40
|
-
reasoning: 0,
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
warnings: [],
|
|
44
|
-
}),
|
|
69
|
+
doStream: async () => create_stream_text_result(title_text),
|
|
45
70
|
});
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
function create_failing_title_model() {
|
|
49
74
|
return new MockLanguageModelV3({
|
|
50
75
|
modelId: "mock-session-title-failing-model",
|
|
51
|
-
|
|
76
|
+
doStream: async () => {
|
|
52
77
|
throw new Error("mock title generation failed");
|
|
53
78
|
},
|
|
54
79
|
});
|
|
@@ -12,6 +12,9 @@ import path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { buildMacOsSeatbeltSandboxEnv } from "@downcity/shell/sandbox/MacOsSeatbeltSandbox.js";
|
|
14
14
|
import { buildLinuxBubblewrapSandboxEnv } from "@downcity/shell/sandbox/LinuxBubblewrapSandbox.js";
|
|
15
|
+
import {
|
|
16
|
+
resolveSandboxEnvAllowlist,
|
|
17
|
+
} from "@downcity/shell/sandbox/SandboxConfigResolver.js";
|
|
15
18
|
|
|
16
19
|
function createParams() {
|
|
17
20
|
const rootPath = "/tmp/downcity-shell-env/project";
|
|
@@ -73,3 +76,18 @@ test("Linux bubblewrap sandbox env points zsh TMPPREFIX at sandbox tmp", () => {
|
|
|
73
76
|
|
|
74
77
|
assertSandboxTmpEnv(env, params.config.tmpDir);
|
|
75
78
|
});
|
|
79
|
+
|
|
80
|
+
test("safe sandbox env allowlist includes explicit agent env keys only", () => {
|
|
81
|
+
const allowlist = resolveSandboxEnvAllowlist({
|
|
82
|
+
rootPath: "/tmp/downcity-shell-env/project",
|
|
83
|
+
env: {
|
|
84
|
+
DYNAMIC_ENV_REPRO: "dynamic_value",
|
|
85
|
+
DC_DYNAMIC_REPRO: "dc_value",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.equal(allowlist.includes("PATH"), true);
|
|
90
|
+
assert.equal(allowlist.includes("DYNAMIC_ENV_REPRO"), true);
|
|
91
|
+
assert.equal(allowlist.includes("DC_DYNAMIC_REPRO"), true);
|
|
92
|
+
assert.equal(allowlist.includes("HOST_ONLY_ENV_REPRO"), false);
|
|
93
|
+
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* - 当 title 仍为空时,后续执行链路可以再次尝试生成。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { streamText, type LanguageModel } from "ai";
|
|
11
|
+
import { buildOpenAIResponsesProviderOptions } from "@executor/messages/SessionMessageCodec.js";
|
|
11
12
|
import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
|
|
12
13
|
import type { SessionRecordV1 } from "@/executor/types/SessionRecords.js";
|
|
13
14
|
import { is_session_message_record } from "@/executor/types/SessionRecords.js";
|
|
@@ -203,8 +204,9 @@ async function generateSessionTitle(input: {
|
|
|
203
204
|
*/
|
|
204
205
|
logger?: Logger;
|
|
205
206
|
}): Promise<string | undefined> {
|
|
207
|
+
let observedStreamError: unknown;
|
|
206
208
|
try {
|
|
207
|
-
const result =
|
|
209
|
+
const result = streamText({
|
|
208
210
|
model: input.model,
|
|
209
211
|
system:
|
|
210
212
|
"You generate minimal conversation titles. Output only the title itself, with no explanation and no quotation marks.",
|
|
@@ -214,8 +216,13 @@ async function generateSessionTitle(input: {
|
|
|
214
216
|
"",
|
|
215
217
|
input.firstUserText,
|
|
216
218
|
].join("\n"),
|
|
219
|
+
providerOptions: buildOpenAIResponsesProviderOptions(),
|
|
220
|
+
onError: ({ error }) => {
|
|
221
|
+
observedStreamError = error;
|
|
222
|
+
},
|
|
217
223
|
});
|
|
218
|
-
const
|
|
224
|
+
const text = await result.text;
|
|
225
|
+
const generatedTitle = normalizeGeneratedTitle(text);
|
|
219
226
|
if (!generatedTitle) {
|
|
220
227
|
await logSessionTitleDiagnostic({
|
|
221
228
|
logger: input.logger,
|
|
@@ -225,12 +232,13 @@ async function generateSessionTitle(input: {
|
|
|
225
232
|
details: {
|
|
226
233
|
modelLabel: input.modelLabel || null,
|
|
227
234
|
firstUserTextLength: input.firstUserText.length,
|
|
228
|
-
rawTitleLength: String(
|
|
235
|
+
rawTitleLength: String(text || "").length,
|
|
229
236
|
},
|
|
230
237
|
});
|
|
231
238
|
}
|
|
232
239
|
return generatedTitle;
|
|
233
240
|
} catch (error) {
|
|
241
|
+
const effectiveError = observedStreamError || error;
|
|
234
242
|
await logSessionTitleDiagnostic({
|
|
235
243
|
logger: input.logger,
|
|
236
244
|
sessionId: input.sessionId,
|
|
@@ -239,7 +247,7 @@ async function generateSessionTitle(input: {
|
|
|
239
247
|
details: {
|
|
240
248
|
modelLabel: input.modelLabel || null,
|
|
241
249
|
firstUserTextLength: input.firstUserText.length,
|
|
242
|
-
...summarizeTitleError(
|
|
250
|
+
...summarizeTitleError(effectiveError),
|
|
243
251
|
},
|
|
244
252
|
});
|
|
245
253
|
// 关键点(中文):标题生成失败不能影响 session 主流程。
|