u1s1-cli 1.4.1 → 1.4.2
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/dist/agent-setup.d.ts +3 -1
- package/dist/agent-setup.js +11 -3
- package/dist/announcements-poll.js +3 -2
- package/dist/api.d.ts +7 -0
- package/dist/api.js +25 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +20 -7
- package/dist/device-auth.d.ts +8 -0
- package/dist/device-auth.js +17 -6
- package/dist/index.js +28 -9
- package/dist/secret-env.d.ts +14 -0
- package/dist/secret-env.js +90 -0
- package/dist/subagent.d.ts +16 -0
- package/dist/subagent.js +22 -3
- package/dist/tools.js +5 -3
- package/dist/update.d.ts +16 -0
- package/dist/update.js +54 -8
- package/dist/workflow/runner.d.ts +2 -0
- package/dist/workflow/runner.js +3 -0
- package/dist/workflow/tool.d.ts +1 -1
- package/dist/workflow/tool.js +4 -1
- package/package.json +1 -1
package/dist/agent-setup.d.ts
CHANGED
|
@@ -97,7 +97,9 @@ export declare function toProviderModels(models: ModelDef[]): {
|
|
|
97
97
|
* 字面量 "local" —— pi 没有凭据会直接拒启("No API key found"),
|
|
98
98
|
* 这是 pi 自带 llama provider 的同款兜底,本地服务会忽略 Authorization 头。
|
|
99
99
|
*/
|
|
100
|
-
export declare function endpointProviderEntry(ep: CustomEndpoint
|
|
100
|
+
export declare function endpointProviderEntry(ep: CustomEndpoint, options?: {
|
|
101
|
+
literalKey?: boolean;
|
|
102
|
+
}): Record<string, unknown>;
|
|
101
103
|
/**
|
|
102
104
|
* 生成 /workflow 提示词模板到 <agentDir>/prompts/workflow.md:引导主 agent
|
|
103
105
|
* 拆解任务 → 生成沙箱脚本 → 调 run_workflow 执行。每次启动重写,便于随版迭代文案。
|
package/dist/agent-setup.js
CHANGED
|
@@ -224,8 +224,12 @@ export function writeWebToolsExtension(cfg, features) {
|
|
|
224
224
|
` const workflow = await import(${JSON.stringify(new URL("./workflow/tool.js", import.meta.url).href)});\n` +
|
|
225
225
|
` pi.registerTool(workflow.createRunWorkflowTool(getParentModel));\n` +
|
|
226
226
|
` }\n`;
|
|
227
|
+
// 密钥剥离守卫也从这里装:Desktop App 的 pi 跑在 pi-web-ui 子进程里,没有
|
|
228
|
+
// 启动器代码,只有扩展能替它把 U1S1_API_KEY / U1S1_EP_KEY_* 挡在子进程之外
|
|
229
|
+
const secretEnvUrl = new URL("./secret-env.js", import.meta.url).href;
|
|
227
230
|
writeFileSync(join(dir, "u1s1-tools.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
228
231
|
`export default async function (pi) {\n` +
|
|
232
|
+
` (await import(${JSON.stringify(secretEnvUrl)})).installChildEnvGuard();\n` +
|
|
229
233
|
` if (process.env.U1S1_TOOLS_VIA_EXTENSION !== "1") return;\n` +
|
|
230
234
|
` const baseUrl = process.env.U1S1_SIGNING_PROXY_URL || ${JSON.stringify(cfg.baseUrl)};\n` +
|
|
231
235
|
` const tools = await import(${JSON.stringify(toolsUrl)});\n` +
|
|
@@ -413,8 +417,10 @@ export default async function (pi) {
|
|
|
413
417
|
}
|
|
414
418
|
|
|
415
419
|
const cwd = process.cwd();
|
|
420
|
+
// 模型跑的 shell 命令拿不到 U1S1_API_KEY / U1S1_EP_KEY_* 等密钥(进程级守卫之外的显式一层)
|
|
421
|
+
const { stripSecretEnv } = await import(${JSON.stringify(new URL("./secret-env.js", import.meta.url).href)});
|
|
416
422
|
hideTool("read", createReadTool(cwd));
|
|
417
|
-
hideTool("bash", createBashTool(cwd));
|
|
423
|
+
hideTool("bash", createBashTool(cwd, { spawnHook: (spawnContext) => ({ ...spawnContext, env: stripSecretEnv(spawnContext.env) }) }));
|
|
418
424
|
hideTool("edit", createEditTool(cwd));
|
|
419
425
|
hideTool("write", createWriteTool(cwd));
|
|
420
426
|
hideTool("grep", createGrepTool(cwd));
|
|
@@ -494,12 +500,14 @@ export function toProviderModels(models) {
|
|
|
494
500
|
* 字面量 "local" —— pi 没有凭据会直接拒启("No API key found"),
|
|
495
501
|
* 这是 pi 自带 llama provider 的同款兜底,本地服务会忽略 Authorization 头。
|
|
496
502
|
*/
|
|
497
|
-
export function endpointProviderEntry(ep) {
|
|
503
|
+
export function endpointProviderEntry(ep, options = {}) {
|
|
504
|
+
// 进程内 registerProvider 可以直接给字面量(不落文件);写 models.json 时仍走 $VAR 引用
|
|
505
|
+
const envRef = `$${endpointKeyEnvName(ep.id)}`;
|
|
498
506
|
return {
|
|
499
507
|
name: ep.name,
|
|
500
508
|
baseUrl: ep.baseUrl,
|
|
501
509
|
api: ep.api,
|
|
502
|
-
apiKey: ep.apiKey ?
|
|
510
|
+
apiKey: ep.apiKey ? (options.literalKey ? ep.apiKey : envRef) : "local",
|
|
503
511
|
models: toProviderModels(ep.models),
|
|
504
512
|
};
|
|
505
513
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchLatestAnnouncement } from "./api.js";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
3
|
import { hasDeviceCredential } from "./device-auth.js";
|
|
4
4
|
/**
|
|
@@ -34,7 +34,8 @@ export function startAnnouncementPoll(deps) {
|
|
|
34
34
|
const cfg = loadConfig();
|
|
35
35
|
if (!hasDeviceCredential(cfg))
|
|
36
36
|
throw new Error("not logged in");
|
|
37
|
-
|
|
37
|
+
// 轻量公告接口,不再每 5 分钟拉整份模型目录(鉴权 + 主库写 + 目录构建)
|
|
38
|
+
return fetchLatestAnnouncement(cfg);
|
|
38
39
|
});
|
|
39
40
|
const tracker = createAnnouncementTracker();
|
|
40
41
|
const tick = async () => {
|
package/dist/api.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export interface ModelsResponse {
|
|
|
64
64
|
export declare class AuthError extends Error {
|
|
65
65
|
}
|
|
66
66
|
export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
|
|
67
|
+
/**
|
|
68
|
+
* 会话内公告轮询用的轻量接口:免鉴权、服务端有缓存,回的就是 /v1/models 里的 announcement
|
|
69
|
+
* 字段。老网关没有这个路由(404)时回退整份 /v1/models。
|
|
70
|
+
*/
|
|
71
|
+
export declare function fetchLatestAnnouncement(cfg: CliConfig): Promise<{
|
|
72
|
+
announcement?: ApiAnnouncement | null;
|
|
73
|
+
}>;
|
|
67
74
|
export interface ApiEndpoint {
|
|
68
75
|
id: string;
|
|
69
76
|
name: string;
|
package/dist/api.js
CHANGED
|
@@ -91,6 +91,31 @@ export async function fetchModels(cfg) {
|
|
|
91
91
|
clientAttestationExpiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : undefined,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* 会话内公告轮询用的轻量接口:免鉴权、服务端有缓存,回的就是 /v1/models 里的 announcement
|
|
96
|
+
* 字段。老网关没有这个路由(404)时回退整份 /v1/models。
|
|
97
|
+
*/
|
|
98
|
+
export async function fetchLatestAnnouncement(cfg) {
|
|
99
|
+
let resp;
|
|
100
|
+
try {
|
|
101
|
+
resp = await fetch(new URL("/public/announcements/latest", cfg.baseUrl), {
|
|
102
|
+
headers: { "x-u1s1-version": VERSION, Accept: "application/json" },
|
|
103
|
+
signal: AbortSignal.timeout(15_000),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new Error(`连不上 ${cfg.baseUrl}`);
|
|
108
|
+
}
|
|
109
|
+
if (resp.status === 404)
|
|
110
|
+
return fetchModels(cfg);
|
|
111
|
+
if (!resp.ok)
|
|
112
|
+
throw new Error(`服务端返回 ${resp.status}`);
|
|
113
|
+
const body = jsonRecord(await readJsonResponseCapped(resp));
|
|
114
|
+
const announcement = body?.announcement;
|
|
115
|
+
return {
|
|
116
|
+
announcement: announcement && typeof announcement === "object" ? announcement : null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
94
119
|
/**
|
|
95
120
|
* 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
|
|
96
121
|
* 老网关没有这个路由(GET 落到静态资产 404),调用方失败时回退本地缓存。
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type webcrypto } from "node:crypto";
|
|
2
2
|
export declare const VERSION: string;
|
|
3
3
|
/**
|
|
4
4
|
* 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
|
|
@@ -165,6 +165,11 @@ export declare function writeAgentDefaultModel(provider: string, modelId: string
|
|
|
165
165
|
*/
|
|
166
166
|
export declare function resolvePreferredModel(cfg: Pick<CliConfig, "model" | "modelProvider">): ModelRef;
|
|
167
167
|
export declare function loadConfig(): CliConfig;
|
|
168
|
+
/**
|
|
169
|
+
* 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
|
|
170
|
+
* (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
|
|
171
|
+
*/
|
|
172
|
+
export declare function writePrivateFile(path: string, content: string): void;
|
|
168
173
|
export declare function saveConfig(cfg: CliConfig): void;
|
|
169
174
|
/** Persist the user's preferred model to both stores. */
|
|
170
175
|
export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { basename, dirname, join } from "node:path";
|
|
@@ -297,10 +298,24 @@ export function loadConfig() {
|
|
|
297
298
|
modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
|
|
298
299
|
};
|
|
299
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
|
|
303
|
+
* (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
|
|
304
|
+
*/
|
|
305
|
+
export function writePrivateFile(path, content) {
|
|
306
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
307
|
+
const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
308
|
+
writeFileSync(tmp, content, { mode: 0o600 });
|
|
309
|
+
try {
|
|
310
|
+
renameSync(tmp, path);
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
rmSync(tmp, { force: true });
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
300
317
|
export function saveConfig(cfg) {
|
|
301
|
-
|
|
302
|
-
writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n");
|
|
303
|
-
chmodSync(configFile, 0o600);
|
|
318
|
+
writePrivateFile(configFile, JSON.stringify(cfg, null, 2) + "\n");
|
|
304
319
|
}
|
|
305
320
|
/** Persist the user's preferred model to both stores. */
|
|
306
321
|
export function persistPreferredModel(cfg, provider, modelId) {
|
|
@@ -312,9 +327,7 @@ export function persistPreferredModel(cfg, provider, modelId) {
|
|
|
312
327
|
// ---- 端点本地缓存:离线/网关不可达时沿用上次拉到的配置(含密钥,0600)----
|
|
313
328
|
const endpointsCacheFile = join(u1s1Dir, "endpoints.json");
|
|
314
329
|
export function saveEndpointsCache(endpoints) {
|
|
315
|
-
|
|
316
|
-
writeFileSync(endpointsCacheFile, JSON.stringify(endpoints, null, 2) + "\n");
|
|
317
|
-
chmodSync(endpointsCacheFile, 0o600);
|
|
330
|
+
writePrivateFile(endpointsCacheFile, JSON.stringify(endpoints, null, 2) + "\n");
|
|
318
331
|
}
|
|
319
332
|
export function loadEndpointsCache() {
|
|
320
333
|
const raw = readJsonFile(endpointsCacheFile);
|
package/dist/device-auth.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ export declare function generateDeviceKeyPair(): Promise<{
|
|
|
9
9
|
export declare function dpopHeaders(cfg: CliConfig, method: string, url: string): Promise<Record<string, string>>;
|
|
10
10
|
/** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
|
|
11
11
|
export declare function authorizedFetch(cfg: CliConfig, input: string | URL, init?: RequestInit): Promise<Response>;
|
|
12
|
+
/**
|
|
13
|
+
* Headers to replay on the loopback response. fetch() has already decoded the
|
|
14
|
+
* upstream body (gzip/br), so the upstream framing and content-encoding must
|
|
15
|
+
* not be forwarded: replaying `content-encoding` made the local SDK inflate
|
|
16
|
+
* plain JSON and surface every non-streaming error as `<status> terminated`
|
|
17
|
+
* (zlib "incorrect header check"), hiding the real message.
|
|
18
|
+
*/
|
|
19
|
+
export declare function forwardedResponseHeaders(upstream: Headers): Record<string, string>;
|
|
12
20
|
/** Buffer a loopback request only up to the same JSON limit enforced by Gateway. */
|
|
13
21
|
export declare function readSigningProxyRequestBody(request: IncomingMessage, maxBytes?: number): Promise<Buffer | undefined>;
|
|
14
22
|
/**
|
package/dist/device-auth.js
CHANGED
|
@@ -80,6 +80,22 @@ export async function authorizedFetch(cfg, input, init = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
return fetch(url, { ...init, headers });
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Headers to replay on the loopback response. fetch() has already decoded the
|
|
85
|
+
* upstream body (gzip/br), so the upstream framing and content-encoding must
|
|
86
|
+
* not be forwarded: replaying `content-encoding` made the local SDK inflate
|
|
87
|
+
* plain JSON and surface every non-streaming error as `<status> terminated`
|
|
88
|
+
* (zlib "incorrect header check"), hiding the real message.
|
|
89
|
+
*/
|
|
90
|
+
export function forwardedResponseHeaders(upstream) {
|
|
91
|
+
const headers = {};
|
|
92
|
+
upstream.forEach((value, name) => {
|
|
93
|
+
if (!["content-length", "transfer-encoding", "connection", "content-encoding"].includes(name)) {
|
|
94
|
+
headers[name] = value;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return headers;
|
|
98
|
+
}
|
|
83
99
|
function requestHeaders(input) {
|
|
84
100
|
const headers = new Headers();
|
|
85
101
|
for (const [name, value] of Object.entries(input)) {
|
|
@@ -247,12 +263,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
|
|
|
247
263
|
body,
|
|
248
264
|
signal: upstreamAbort.signal,
|
|
249
265
|
});
|
|
250
|
-
|
|
251
|
-
upstream.headers.forEach((value, name) => {
|
|
252
|
-
if (!["content-length", "transfer-encoding", "connection"].includes(name))
|
|
253
|
-
headers[name] = value;
|
|
254
|
-
});
|
|
255
|
-
res.writeHead(upstream.status, headers);
|
|
266
|
+
res.writeHead(upstream.status, forwardedResponseHeaders(upstream.headers));
|
|
256
267
|
if (upstream.body) {
|
|
257
268
|
for await (const chunk of upstream.body)
|
|
258
269
|
res.write(chunk);
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ensureUsableShell } from "./shell-doctor.js";
|
|
|
11
11
|
import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
|
|
12
12
|
import { AuthError, fetchModels, loadCustomEndpoints, readJsonResponseCapped } from "./api.js";
|
|
13
13
|
import { ensureSigningProxy } from "./device-auth.js";
|
|
14
|
+
import { installChildEnvGuard } from "./secret-env.js";
|
|
14
15
|
const PACKAGE_NAME = "u1s1-cli";
|
|
15
16
|
/** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
|
|
16
17
|
let pendingUpdate;
|
|
@@ -145,9 +146,18 @@ function ensureTmuxKeyboardProtocol() {
|
|
|
145
146
|
runTmux(["bind-key", "-n", "S-Enter", "send-keys", "-l", "\x1b[13;2u"]);
|
|
146
147
|
runTmux(["bind-key", "-n", "C-Enter", "send-keys", "-l", "\x1b[13;5u"]);
|
|
147
148
|
}
|
|
149
|
+
/** 取模型列表的请求发出后、发起 pi import 前的让路时间(见 runAgent 内注释)。 */
|
|
150
|
+
const AGENT_IMPORT_DEFER_MS = 200;
|
|
148
151
|
async function runAgent(cfg, args) {
|
|
149
152
|
cleanupBrandThemes();
|
|
150
153
|
ensureDefaultSettings();
|
|
154
|
+
// pi 在 import/初始化阶段可能读到的常量环境变量先设好(import 在下面网络请求
|
|
155
|
+
// 发出之后才发起);依赖网络结果的变量(签名代理地址/密钥、端点密钥)仍在进 pi
|
|
156
|
+
// 之前才设置——pi 只在调用时读它们。
|
|
157
|
+
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
|
158
|
+
process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
|
|
159
|
+
// hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
|
|
160
|
+
process.env["PI_SKIP_VERSION_CHECK"] = "1";
|
|
151
161
|
// Windows shell 体检异步先行,与取模型列表/搜索工具下载并行;首启验活
|
|
152
162
|
// (Defender 首扫 bash.exe 可达秒级)藏进网络等待,进 pi 前再 await
|
|
153
163
|
const shellReady = ensureUsableShell();
|
|
@@ -163,9 +173,18 @@ async function runAgent(cfg, args) {
|
|
|
163
173
|
// 老网关没有 /v1/image,image_gen 缺失时按关闭处理,不注册生图工具
|
|
164
174
|
let imageGenEnabled = false;
|
|
165
175
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
176
|
+
const modelsReady = fetchModels(cfg);
|
|
177
|
+
modelsReady.catch(() => { });
|
|
178
|
+
// pi 的模块图 import 要 300~800 ms,且 ESM 求值是同步的、会把事件循环卡住;
|
|
179
|
+
// 若在请求发出前就 import,请求要等它求值完才真正上线,重叠白做(实测总耗时不变)。
|
|
180
|
+
// 先让 DNS/TLS/写出在事件循环里推进一段,再发起 import,让它落在等服务端应答的空档里。
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, AGENT_IMPORT_DEFER_MS));
|
|
182
|
+
const agentReady = import("@earendil-works/pi-coding-agent");
|
|
183
|
+
// 失败留到下面 await 时再抛;这里只是避免在等网络期间被当成未处理的 rejection。
|
|
184
|
+
agentReady.catch(() => { });
|
|
166
185
|
let modelsResp;
|
|
167
186
|
try {
|
|
168
|
-
modelsResp = await
|
|
187
|
+
modelsResp = await modelsReady;
|
|
169
188
|
}
|
|
170
189
|
catch (e) {
|
|
171
190
|
if (e instanceof AuthError) {
|
|
@@ -228,19 +247,18 @@ async function runAgent(cfg, args) {
|
|
|
228
247
|
// OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
|
|
229
248
|
writeAttributionExtension();
|
|
230
249
|
ensureTmuxKeyboardProtocol();
|
|
231
|
-
// must be set before pi reads them (getAgentDir() reads at call time
|
|
232
|
-
|
|
250
|
+
// must be set before pi reads them (getAgentDir() reads at call time);
|
|
251
|
+
// 常量部分已在 runAgent 开头、发起 import 之前设好
|
|
233
252
|
process.env["U1S1_API_KEY"] = officialCfg.apiKey;
|
|
234
253
|
process.env["U1S1_SIGNING_PROXY_URL"] = officialCfg.baseUrl;
|
|
235
|
-
process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
|
|
236
254
|
// 自定义端点的密钥走环境变量引用(models.json 里只有 $VAR,不落明文)
|
|
237
255
|
Object.assign(process.env, endpointKeyEnv());
|
|
238
256
|
// 剔掉环境里的其他厂商密钥,/model 只展示 u1s1 官方模型 + 自定义端点
|
|
239
257
|
scrubForeignProviderEnv();
|
|
240
|
-
//
|
|
241
|
-
|
|
258
|
+
// 密钥只留在本进程:模型跑的 shell 命令、子 agent、扩展派生的任何子进程都拿不到
|
|
259
|
+
installChildEnvGuard();
|
|
242
260
|
await searchToolsReady;
|
|
243
|
-
const { CustomEditor, main } = await
|
|
261
|
+
const { CustomEditor, main } = await agentReady;
|
|
244
262
|
// 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
|
|
245
263
|
// 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
|
|
246
264
|
// Shift+Enter,输入框就会换行,真·Alt+Enter(\x1b[13;3u)不受影响。
|
|
@@ -347,16 +365,17 @@ async function runAgent(cfg, args) {
|
|
|
347
365
|
});
|
|
348
366
|
},
|
|
349
367
|
});
|
|
368
|
+
// 进程内注册直接给字面量密钥:不经 $VAR 环境变量引用,也不落文件
|
|
350
369
|
pi.registerProvider(PROVIDER_ID, {
|
|
351
370
|
name: "u1s1",
|
|
352
371
|
baseUrl: officialCfg.baseUrl,
|
|
353
372
|
api: "openai-completions",
|
|
354
|
-
apiKey:
|
|
373
|
+
apiKey: officialCfg.apiKey,
|
|
355
374
|
models: toProviderModels(MODELS),
|
|
356
375
|
});
|
|
357
376
|
// 用户在云端配置的自定义端点,一个端点一个 provider,/model 里即可切换
|
|
358
377
|
for (const ep of CUSTOM_ENDPOINTS) {
|
|
359
|
-
pi.registerProvider(ep.id, endpointProviderEntry(ep));
|
|
378
|
+
pi.registerProvider(ep.id, endpointProviderEntry(ep, { literalKey: true }));
|
|
360
379
|
}
|
|
361
380
|
// /model and Ctrl+P already write pi settings; also keep ~/.u1s1/config.json in sync
|
|
362
381
|
pi.on("model_select", (event) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function isSecretEnvName(name: string): boolean;
|
|
2
|
+
/** 返回去掉密钥变量的环境副本;传入对象本身不动。 */
|
|
3
|
+
export declare function stripSecretEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
4
|
+
/**
|
|
5
|
+
* 把 child_process 各函数的实参改写成「env 已剥离密钥」的版本:已有 options
|
|
6
|
+
* 就替换其 env(缺省 env 视为 process.env);没有 options 就在 args 数组之后、
|
|
7
|
+
* 回调之前插一个。签名族:fn(cmd[, args][, options][, callback])。
|
|
8
|
+
*/
|
|
9
|
+
export declare function withStrippedEnvArgs(args: unknown[]): unknown[];
|
|
10
|
+
/**
|
|
11
|
+
* 给 node:child_process 装上密钥剥离守卫(幂等)。ESM 的具名导入是活绑定,
|
|
12
|
+
* syncBuiltinESMExports 之后 `import { spawn } from "child_process"` 也拿到守卫版。
|
|
13
|
+
*/
|
|
14
|
+
export declare function installChildEnvGuard(): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
/**
|
|
5
|
+
* 只该留在 u1s1 进程内的凭据类环境变量:官方 key(TUI 里是本地签名代理的
|
|
6
|
+
* 一次性 bearer)、云端自定义端点的第三方长期 key、设备令牌与设备私钥。
|
|
7
|
+
* pi 通过 models.json / auth.json 里的 `$VAR` 引用在每次请求时从 process.env
|
|
8
|
+
* 解析它们,所以进程内必须保留;但模型执行的任何 shell 命令都不该看见 ——
|
|
9
|
+
* prompt injection 跑一句 `env` 或项目 `postinstall` 就能把它们外传。
|
|
10
|
+
*
|
|
11
|
+
* 非密钥的 U1S1_* 配置(签名代理地址、工具开关、子 agent 标记、BASE_URL、
|
|
12
|
+
* CLIENT、MODELS_PATH、设备公钥)照常继承:脚本可能正当依赖它们,且单独
|
|
13
|
+
* 拿到也换不来任何权限(代理地址没有 bearer 打不通)。
|
|
14
|
+
*/
|
|
15
|
+
const SECRET_ENV_NAMES = new Set(["U1S1_API_KEY", "U1S1_DEVICE_TOKEN", "U1S1_DEVICE_PRIVATE_JWK"]);
|
|
16
|
+
const SECRET_ENV_PREFIXES = ["U1S1_EP_KEY_"];
|
|
17
|
+
export function isSecretEnvName(name) {
|
|
18
|
+
return SECRET_ENV_NAMES.has(name) || SECRET_ENV_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
19
|
+
}
|
|
20
|
+
/** 返回去掉密钥变量的环境副本;传入对象本身不动。 */
|
|
21
|
+
export function stripSecretEnv(env = process.env) {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [key, value] of Object.entries(env)) {
|
|
24
|
+
if (!isSecretEnvName(key))
|
|
25
|
+
out[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
const GUARDED_METHODS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"];
|
|
30
|
+
const GUARD_MARK = Symbol.for("u1s1.childEnvGuard");
|
|
31
|
+
function isOptionsObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 把 child_process 各函数的实参改写成「env 已剥离密钥」的版本:已有 options
|
|
36
|
+
* 就替换其 env(缺省 env 视为 process.env);没有 options 就在 args 数组之后、
|
|
37
|
+
* 回调之前插一个。签名族:fn(cmd[, args][, options][, callback])。
|
|
38
|
+
*/
|
|
39
|
+
export function withStrippedEnvArgs(args) {
|
|
40
|
+
const next = [...args];
|
|
41
|
+
for (let i = 1; i < next.length; i++) {
|
|
42
|
+
const candidate = next[i];
|
|
43
|
+
if (!isOptionsObject(candidate))
|
|
44
|
+
continue;
|
|
45
|
+
const env = candidate["env"];
|
|
46
|
+
next[i] = { ...candidate, env: stripSecretEnv(isOptionsObject(env) ? env : process.env) };
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
const at = Array.isArray(next[1]) ? 2 : 1;
|
|
50
|
+
next.splice(at, 0, { env: stripSecretEnv() });
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
function isGuarded(fn) {
|
|
54
|
+
return fn[GUARD_MARK] === true;
|
|
55
|
+
}
|
|
56
|
+
/** exec/execFile 的 util.promisify 定制:必须经过守卫版本,否则 promisify 会绕回原函数。 */
|
|
57
|
+
function promisifiedThrough(fn) {
|
|
58
|
+
return (...args) => new Promise((resolve, reject) => {
|
|
59
|
+
fn(...args, (error, stdout, stderr) => {
|
|
60
|
+
if (error) {
|
|
61
|
+
error.stdout = stdout;
|
|
62
|
+
error.stderr = stderr;
|
|
63
|
+
reject(error);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
resolve({ stdout, stderr });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 给 node:child_process 装上密钥剥离守卫(幂等)。ESM 的具名导入是活绑定,
|
|
72
|
+
* syncBuiltinESMExports 之后 `import { spawn } from "child_process"` 也拿到守卫版。
|
|
73
|
+
*/
|
|
74
|
+
export function installChildEnvGuard() {
|
|
75
|
+
const cp = childProcess;
|
|
76
|
+
for (const name of GUARDED_METHODS) {
|
|
77
|
+
const original = cp[name];
|
|
78
|
+
if (!original || isGuarded(original))
|
|
79
|
+
continue;
|
|
80
|
+
const guarded = function (...args) {
|
|
81
|
+
return original.apply(this, withStrippedEnvArgs(args));
|
|
82
|
+
};
|
|
83
|
+
Object.defineProperty(guarded, GUARD_MARK, { value: true });
|
|
84
|
+
if (name === "exec" || name === "execFile") {
|
|
85
|
+
Object.defineProperty(guarded, promisify.custom, { value: promisifiedThrough(guarded) });
|
|
86
|
+
}
|
|
87
|
+
cp[name] = guarded;
|
|
88
|
+
}
|
|
89
|
+
syncBuiltinESMExports();
|
|
90
|
+
}
|
package/dist/subagent.d.ts
CHANGED
|
@@ -19,7 +19,23 @@ export interface SubagentOptions {
|
|
|
19
19
|
noTools?: boolean;
|
|
20
20
|
/** 子 agent 的工作目录(git worktree 隔离等场景);缺省继承主进程 cwd。 */
|
|
21
21
|
cwd?: string;
|
|
22
|
+
/**
|
|
23
|
+
* 主会话对项目的信任裁决(ctx.isProjectTrusted());缺省按 trust.json 非交互
|
|
24
|
+
* 裁决。子 agent 绝不能默认可信:主会话拒绝过的 .pi/extensions 不能被绕过。
|
|
25
|
+
*/
|
|
26
|
+
projectTrusted?: boolean;
|
|
27
|
+
/** 信任裁决依据的目录(worktree 隔离时是源仓库,不是临时工作树);缺省 cwd。 */
|
|
28
|
+
trustCwd?: string;
|
|
22
29
|
}
|
|
30
|
+
/** 工具 execute 的 ctx 里的信任裁决;老版 pi 没这个方法时返回 undefined 走存档兜底。 */
|
|
31
|
+
export declare function contextProjectTrust(ctx: {
|
|
32
|
+
isProjectTrusted?: () => boolean;
|
|
33
|
+
} | undefined): boolean | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* 与 pi 主会话同一口径的非交互裁决:没有需要信任门的项目资源(.pi/extensions、
|
|
36
|
+
* .agents/skills 等)即视为可信;有则只认 trust.json 里的明确「信任」,从不弹窗。
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveStoredProjectTrust(cwd: string, agentDir?: string): boolean;
|
|
23
39
|
/** 单个子 agent 的 token/费用用量(汇总自会话内全部 assistant 消息)。 */
|
|
24
40
|
export interface SubagentUsage {
|
|
25
41
|
totalTokens: number;
|
package/dist/subagent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, hasTrustRequiringProjectResources, ModelRuntime, ProjectTrustStore, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
// ---- 子 agent spawn 基础设施:spawn_subagent 工具与 workflow runner 共用 ----
|
|
3
3
|
/** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
|
|
4
4
|
export const SUBAGENT_CONCURRENCY = 4;
|
|
@@ -6,6 +6,19 @@ export const SUBAGENT_CONCURRENCY = 4;
|
|
|
6
6
|
export const SUBAGENT_TIMEOUT_MS = 15 * 60_000;
|
|
7
7
|
/** ModelRuntime 进程内只建一次(auth/models 解析有启动开销)。 */
|
|
8
8
|
let sharedModelRuntime;
|
|
9
|
+
/** 工具 execute 的 ctx 里的信任裁决;老版 pi 没这个方法时返回 undefined 走存档兜底。 */
|
|
10
|
+
export function contextProjectTrust(ctx) {
|
|
11
|
+
return typeof ctx?.isProjectTrusted === "function" ? ctx.isProjectTrusted() : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 与 pi 主会话同一口径的非交互裁决:没有需要信任门的项目资源(.pi/extensions、
|
|
15
|
+
* .agents/skills 等)即视为可信;有则只认 trust.json 里的明确「信任」,从不弹窗。
|
|
16
|
+
*/
|
|
17
|
+
export function resolveStoredProjectTrust(cwd, agentDir = getAgentDir()) {
|
|
18
|
+
if (!hasTrustRequiringProjectResources(cwd))
|
|
19
|
+
return true;
|
|
20
|
+
return new ProjectTrustStore(agentDir).get(cwd) === true;
|
|
21
|
+
}
|
|
9
22
|
function cancelledError(signal) {
|
|
10
23
|
return new Error("已取消", { cause: signal.reason });
|
|
11
24
|
}
|
|
@@ -121,9 +134,15 @@ export async function runSubagent(opts) {
|
|
|
121
134
|
}
|
|
122
135
|
// 子会话不加载精简 UI 扩展:它用 process.cwd() 重建内置工具,会破坏自定义
|
|
123
136
|
// cwd(worktree 隔离)下的路径解析;且 UI 美化对无界面的子 agent 毫无意义
|
|
137
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
138
|
+
const agentDir = getAgentDir();
|
|
139
|
+
// 项目信任跟随主会话:SettingsManager 缺省 projectTrusted=true,会把主会话
|
|
140
|
+
// 拒绝过的 .pi/extensions 在子 agent 里悄悄加载执行
|
|
141
|
+
const projectTrusted = opts.projectTrusted ?? resolveStoredProjectTrust(opts.trustCwd ?? cwd, agentDir);
|
|
124
142
|
const loader = new DefaultResourceLoader({
|
|
125
|
-
agentDir
|
|
126
|
-
cwd
|
|
143
|
+
agentDir,
|
|
144
|
+
cwd,
|
|
145
|
+
settingsManager: SettingsManager.create(cwd, agentDir, { projectTrusted }),
|
|
127
146
|
extensionsOverride: (base) => ({
|
|
128
147
|
...base,
|
|
129
148
|
extensions: base.extensions.filter((e) => !JSON.stringify(e).includes("u1s1-compact-ui")),
|
package/dist/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, extname, resolve } from "node:path";
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { runPool, runSubagent, SUBAGENT_CONCURRENCY, SUBAGENT_TIMEOUT_MS } from "./subagent.js";
|
|
4
|
+
import { contextProjectTrust, runPool, runSubagent, SUBAGENT_CONCURRENCY, SUBAGENT_TIMEOUT_MS, } from "./subagent.js";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { generateImage, renderPage, searchWeb } from "./api.js";
|
|
@@ -204,7 +204,9 @@ export function createSubagentTool(getParentModel) {
|
|
|
204
204
|
: "🤖 子任务";
|
|
205
205
|
return compactResultRender({ result, options, theme, context, summaryLine: summary });
|
|
206
206
|
},
|
|
207
|
-
async execute(_toolCallId, params, signal) {
|
|
207
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
208
|
+
// 子 agent 沿用主会话的项目信任裁决,不能默认可信
|
|
209
|
+
const projectTrusted = contextProjectTrust(ctx);
|
|
208
210
|
const list = (params.tasks?.length ? params.tasks : params.task ? [params.task] : [])
|
|
209
211
|
.map((t) => t.trim())
|
|
210
212
|
.filter(Boolean)
|
|
@@ -224,7 +226,7 @@ export function createSubagentTool(getParentModel) {
|
|
|
224
226
|
items: list,
|
|
225
227
|
limit: SUBAGENT_CONCURRENCY,
|
|
226
228
|
run: async (task, i) => {
|
|
227
|
-
outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
|
|
229
|
+
outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal, projectTrusted }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
|
|
228
230
|
},
|
|
229
231
|
signal,
|
|
230
232
|
});
|
package/dist/update.d.ts
CHANGED
|
@@ -5,4 +5,20 @@ export declare function detectPackageManager(): string;
|
|
|
5
5
|
/** Fetch the latest published version from npm registry. */
|
|
6
6
|
export declare function getLatestVersion(): Promise<string | undefined>;
|
|
7
7
|
export declare function compareVersions(a: string, b: string): number;
|
|
8
|
+
/** 从固定发布源拉一个文本文件:强制 https、拒绝重定向、限长。 */
|
|
9
|
+
export declare function fetchReleaseText(name: string, maxBytes: number, timeoutMs?: number): Promise<string>;
|
|
10
|
+
/** 在 sha256sum 格式的清单里找某个文件的哈希("<hex> <name>" 或 "<hex> *<name>")。 */
|
|
11
|
+
export declare function findPublishedSha256(sums: string, name: string): string | undefined;
|
|
12
|
+
export type InstallScriptCheck = "verified" | "unlisted" | "mismatch";
|
|
13
|
+
/** 拿发布清单核对脚本正文;清单没列脚本(旧版发布)返回 unlisted,由调用方决定是否放行。 */
|
|
14
|
+
export declare function verifyInstallScript(script: string, sums: string, name?: string): InstallScriptCheck;
|
|
15
|
+
/**
|
|
16
|
+
* 下载安装脚本并对照随发布上传的 SHA256SUMS。清单拉不到或哈希不符都拒绝执行;
|
|
17
|
+
* 清单里没有脚本条目只可能是旧版发布,放行但告知调用方(verified=false)。
|
|
18
|
+
* 这只是同源完整性校验(防边缘缓存残缺/单文件被换),不是代码签名。
|
|
19
|
+
*/
|
|
20
|
+
export declare function downloadInstallScript(): Promise<{
|
|
21
|
+
script: string;
|
|
22
|
+
verified: boolean;
|
|
23
|
+
}>;
|
|
8
24
|
export declare function update(): Promise<void>;
|
package/dist/update.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
@@ -64,6 +65,52 @@ export function compareVersions(a, b) {
|
|
|
64
65
|
}
|
|
65
66
|
return 0;
|
|
66
67
|
}
|
|
68
|
+
/** 发布件只认这一个 https 源;任何跳转(哪怕同域)都视为异常,不跟。 */
|
|
69
|
+
const RELEASE_ORIGIN = "https://u1s1.io";
|
|
70
|
+
const INSTALL_SCRIPT_NAME = "install.sh";
|
|
71
|
+
const MAX_SUMS_BYTES = 64 * 1024;
|
|
72
|
+
/** 从固定发布源拉一个文本文件:强制 https、拒绝重定向、限长。 */
|
|
73
|
+
export async function fetchReleaseText(name, maxBytes, timeoutMs = 30_000) {
|
|
74
|
+
const url = new URL(`/releases/${name}`, RELEASE_ORIGIN);
|
|
75
|
+
if (url.protocol !== "https:")
|
|
76
|
+
throw new Error(`拒绝非 https 发布源: ${url.href}`);
|
|
77
|
+
const res = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`${name}: HTTP ${res.status}`);
|
|
80
|
+
return readResponseTextCapped(res, maxBytes);
|
|
81
|
+
}
|
|
82
|
+
/** 在 sha256sum 格式的清单里找某个文件的哈希("<hex> <name>" 或 "<hex> *<name>")。 */
|
|
83
|
+
export function findPublishedSha256(sums, name) {
|
|
84
|
+
for (const line of sums.split("\n")) {
|
|
85
|
+
const m = /^([0-9a-fA-F]{64})\s+\*?(.+?)\s*$/.exec(line.trim());
|
|
86
|
+
if (m && m[2] === name)
|
|
87
|
+
return m[1].toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** 拿发布清单核对脚本正文;清单没列脚本(旧版发布)返回 unlisted,由调用方决定是否放行。 */
|
|
92
|
+
export function verifyInstallScript(script, sums, name = INSTALL_SCRIPT_NAME) {
|
|
93
|
+
const expected = findPublishedSha256(sums, name);
|
|
94
|
+
if (!expected)
|
|
95
|
+
return "unlisted";
|
|
96
|
+
const actual = createHash("sha256").update(script, "utf8").digest("hex");
|
|
97
|
+
return actual === expected ? "verified" : "mismatch";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 下载安装脚本并对照随发布上传的 SHA256SUMS。清单拉不到或哈希不符都拒绝执行;
|
|
101
|
+
* 清单里没有脚本条目只可能是旧版发布,放行但告知调用方(verified=false)。
|
|
102
|
+
* 这只是同源完整性校验(防边缘缓存残缺/单文件被换),不是代码签名。
|
|
103
|
+
*/
|
|
104
|
+
export async function downloadInstallScript() {
|
|
105
|
+
const [script, sums] = await Promise.all([
|
|
106
|
+
fetchReleaseText(INSTALL_SCRIPT_NAME, MAX_INSTALL_SCRIPT_BYTES),
|
|
107
|
+
fetchReleaseText("SHA256SUMS", MAX_SUMS_BYTES, 15_000),
|
|
108
|
+
]);
|
|
109
|
+
const check = verifyInstallScript(script, sums);
|
|
110
|
+
if (check === "mismatch")
|
|
111
|
+
throw new Error("安装脚本与发布清单的 SHA256 不符,已拒绝执行");
|
|
112
|
+
return { script, verified: check === "verified" };
|
|
113
|
+
}
|
|
67
114
|
/**
|
|
68
115
|
* 便携版原地升级:下载官网安装脚本并替用户执行,整包替换安装目录。
|
|
69
116
|
*
|
|
@@ -102,15 +149,14 @@ async function portableSelfUpdate(latest) {
|
|
|
102
149
|
console.log(`正在下载安装脚本并升级到 v${latest}…`);
|
|
103
150
|
let script;
|
|
104
151
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
script = await readResponseTextCapped(res, MAX_INSTALL_SCRIPT_BYTES);
|
|
152
|
+
const download = await downloadInstallScript();
|
|
153
|
+
script = download.script;
|
|
154
|
+
if (!download.verified) {
|
|
155
|
+
console.log("⚠ 发布清单里没有安装脚本的校验和(旧版发布),跳过脚本校验;安装包本身仍会校验。");
|
|
156
|
+
}
|
|
111
157
|
}
|
|
112
|
-
catch {
|
|
113
|
-
console.error(
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`安装脚本下载失败:${e.message}。请手动运行:`);
|
|
114
160
|
console.error(" curl -fsSL https://u1s1.io/releases/install.sh | bash");
|
|
115
161
|
process.exit(1);
|
|
116
162
|
}
|
|
@@ -49,6 +49,8 @@ export interface WorkflowRunOptions {
|
|
|
49
49
|
progressPath: string;
|
|
50
50
|
resume?: boolean;
|
|
51
51
|
parentModel: ParentModelRef;
|
|
52
|
+
/** 主会话的项目信任裁决;子 agent 与临时 worktree 都沿用它,缺省按 trust.json 非交互裁决。 */
|
|
53
|
+
projectTrusted?: boolean;
|
|
52
54
|
signal?: AbortSignal;
|
|
53
55
|
timeoutMs?: number;
|
|
54
56
|
/** 整个 run 的 token 上限;缺省 WORKFLOW_DEFAULT_BUDGET_TOKENS,<=0 不限。 */
|
package/dist/workflow/runner.js
CHANGED
|
@@ -180,6 +180,9 @@ export async function runWorkflow(opts) {
|
|
|
180
180
|
timeoutMs: PER_TASK_TIMEOUT_MS,
|
|
181
181
|
signal: internal.signal,
|
|
182
182
|
cwd: worktreeDir,
|
|
183
|
+
// 临时 worktree 永远不在 trust.json 里:信任裁决按源仓库(主进程 cwd)算
|
|
184
|
+
projectTrusted: opts.projectTrusted,
|
|
185
|
+
trustCwd: process.cwd(),
|
|
183
186
|
noTools: o.noTools === true,
|
|
184
187
|
});
|
|
185
188
|
stats.ok++;
|
package/dist/workflow/tool.d.ts
CHANGED
package/dist/workflow/tool.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
|
+
import { contextProjectTrust } from "../subagent.js";
|
|
6
7
|
import { compactResultRender, truncate } from "../tools.js";
|
|
7
8
|
import { runWorkflow, saveWorkflowScript, validateScript, workflowsDir, WORKFLOW_DEFAULT_BUDGET_TOKENS, WORKFLOW_TIMEOUT_MS, } from "./runner.js";
|
|
8
9
|
import { buildFromTemplate, TEMPLATES } from "./templates.js";
|
|
@@ -131,7 +132,7 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
131
132
|
return compactResultRender({ result, options, theme, context, summaryLine: summary });
|
|
132
133
|
},
|
|
133
134
|
async execute(...args) {
|
|
134
|
-
const [, params, signal, onUpdate] = args;
|
|
135
|
+
const [, params, signal, onUpdate, ctx] = args;
|
|
135
136
|
const source = resolveWorkflowSource(params);
|
|
136
137
|
const scriptPath = persistWorkflowSource(source);
|
|
137
138
|
const progressPath = scriptPath.replace(/\.mjs$/, ".progress.jsonl");
|
|
@@ -159,6 +160,8 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
159
160
|
progressPath,
|
|
160
161
|
resume: params.resume === true,
|
|
161
162
|
parentModel: getParentModel(),
|
|
163
|
+
// 子 agent(含临时 worktree)沿用主会话的项目信任裁决
|
|
164
|
+
projectTrusted: contextProjectTrust(ctx),
|
|
162
165
|
signal,
|
|
163
166
|
timeoutMs: Math.min(180, params.timeout_minutes ?? WORKFLOW_TIMEOUT_MS / 60_000) * 60_000,
|
|
164
167
|
budgetTokens: params.budget_tokens,
|