From 222c1bdf4e4da5b4fdb3b1b4a5ddf0868b9968e0 Mon Sep 17 00:00:00 2001 From: Chris <4436110+zqchris@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:52:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(pi):=20=E8=AE=A9=200.84.3=20=E7=9A=84=20pow?= =?UTF-8?q?ershell=E3=80=81=E5=8E=8B=E7=BC=A9=E5=A4=B1=E8=B4=A5=E5=92=8C?= =?UTF-8?q?=20xAI=20=E8=B7=AF=E7=94=B1=E4=B8=8D=E7=82=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin 已是 v0.84.3,但 harness 仍按 0.83 的 bash-only / Chat Completions 假设工作。 把 powershell 收进与 bash 相同的审批和 env 隔离,忽略泄漏的 session_compact_failed, 并让假网关按 URL 回答 Responses,避免加密 reasoning 回放把集成测试挂死。 Signed-off-by: Chris <4436110+zqchris@users.noreply.github.com> --- docs/dev-rules/pi-harness.md | 2 +- .../pi/__tests__/cindyBridgeSource.test.ts | 7 +- .../pi/__tests__/pi-agent.integration.test.ts | 27 ++++++-- .../__tests__/pi-auto-review-policy.test.ts | 12 ++++ .../agents/pi/__tests__/pi-translator.test.ts | 17 +++++ .../src/agents/pi/auto-review-policy.ts | 7 +- .../src/agents/pi/cindy-bridge-source.ts | 68 +++++++++++++++++-- .../maker-core/src/agents/pi/translator.ts | 3 + 8 files changed, 131 insertions(+), 12 deletions(-) diff --git a/docs/dev-rules/pi-harness.md b/docs/dev-rules/pi-harness.md index 1d7eda1622..85e83ab540 100644 --- a/docs/dev-rules/pi-harness.md +++ b/docs/dev-rules/pi-harness.md @@ -181,7 +181,7 @@ Claude Code 仍用独立百分比。env:`CINDY_PI_API_KEY`、 ## 6. 上线门禁 -- [x] **平台分发**:pin 已升级到 Pi `v0.83.0`,darwin arm64/x64、linux arm64/x64、 +- [x] **平台分发**:pin 已升级到 Pi `v0.84.3`,darwin arm64/x64、linux arm64/x64、 win32 arm64/x64 六份官方资产都进入 digest pin;下载器兼容 Unix `pi/` 嵌套包与 Windows 根目录平铺 zip。当前 Mac 已完成六资产 SHA-256 下载验收;非本机 OS 的 最终启动 smoke 仍由对应发布 runner 执行。2026-08 起 pi 与 cc/codex 一样只走 diff --git a/packages/maker-core/src/agents/pi/__tests__/cindyBridgeSource.test.ts b/packages/maker-core/src/agents/pi/__tests__/cindyBridgeSource.test.ts index a52c386cae..0843cca785 100644 --- a/packages/maker-core/src/agents/pi/__tests__/cindyBridgeSource.test.ts +++ b/packages/maker-core/src/agents/pi/__tests__/cindyBridgeSource.test.ts @@ -919,7 +919,8 @@ describe('cindy-bridge extension source', () => { hasUnresolvedTarget: false, }); expect(source).toContain("event.toolName === 'bash'\n ? bashInputReadEvidence(event.input)"); - expect(source).toContain('bashReadEvidence.unresolved || touchesCredentialPath(bashReadTargets)'); + expect(source).toContain("event.toolName === 'powershell'\n ? powershellInputReadEvidence(event.input)"); + expect(source).toContain('isCindyShellTool(event.toolName) && (bashReadEvidence.unresolved || touchesCredentialPath(bashReadTargets))'); expect(source).toContain('resolvedCredentialPaths: credentialEvidenceForHost'); } finally { rmSync(tempRoot, { recursive: true, force: true }); @@ -933,6 +934,10 @@ describe('cindy-bridge extension source', () => { for (const tool of ['createBashTool', 'createFindTool', 'createGrepTool', 'createLsTool']) { expect(source).toContain(tool + ','); } + expect(source).toContain('import * as piCodingAgent from'); + expect(source).toContain('createPowerShellTool'); + expect(source).toContain('function isCindyShellTool'); + expect(source).toContain('function powershellInputReadEvidence'); expect(source).toContain("const args = ['--files', '--hidden', '--no-require-git']"); expect(source).toContain("if (pattern.includes('/')) {"); expect(source).toContain('path.basename(relative)'); diff --git a/packages/maker-core/src/agents/pi/__tests__/pi-agent.integration.test.ts b/packages/maker-core/src/agents/pi/__tests__/pi-agent.integration.test.ts index cb7ae72883..abd8e07112 100644 --- a/packages/maker-core/src/agents/pi/__tests__/pi-agent.integration.test.ts +++ b/packages/maker-core/src/agents/pi/__tests__/pi-agent.integration.test.ts @@ -362,7 +362,13 @@ describe.skipIf(!piAvailable)('PiAgent integration (real pi binary + fake gatewa 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }); - res.end(scriptedResponses.shift() ?? anthropicStreamBody('pong from fake gateway')); + const url = req.url ?? ''; + const fallback = url.includes('/responses') + ? responsesStreamBody('pong from fake gateway', 'pi-test-model') + : url.includes('/chat/completions') + ? chatCompletionsStreamBody('pong from fake gateway', 'pi-test-model') + : anthropicStreamBody('pong from fake gateway'); + res.end(scriptedResponses.shift() ?? fallback); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); @@ -761,11 +767,13 @@ describe.skipIf(!piAvailable)('PiAgent integration (real pi binary + fake gatewa responsesStreamBody('pong from xai responses', 'grok-4.5'), '/v1/responses', ); + // Pi v0.84.3 moved bundled xAI models onto Responses with encrypted + // reasoning replay. grok-build-0.1 is no longer Chat Completions. await run( 'itest-native-xai-completions', 'xai/grok-build-0.1', - chatCompletionsStreamBody('pong from xai completions', 'grok-build-0.1'), - '/v1/chat/completions', + responsesStreamBody('pong from xai completions', 'grok-build-0.1'), + '/v1/responses', ); }, ); @@ -1318,7 +1326,18 @@ describe.skipIf(!piAvailable)('PiAgent integration (real pi binary + fake gatewa const activeConfigHomes = readdirSync(runTmp, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => path.join(runTmp, entry.name)) - .filter((candidate) => existsSync(path.join(candidate, 'models.json'))); + .filter((candidate) => { + const modelsPath = path.join(candidate, 'models.json'); + if (!existsSync(modelsPath)) return false; + try { + const parsed = JSON.parse(readFileSync(modelsPath, 'utf8')) as { + providers?: Record; + }; + return Boolean(parsed.providers?.localbyom); + } catch { + return false; + } + }); expect(activeConfigHomes).toHaveLength(1); const configHome = activeConfigHomes[0]; if (!configHome) throw new Error('active Pi config home missing'); diff --git a/packages/maker-core/src/agents/pi/__tests__/pi-auto-review-policy.test.ts b/packages/maker-core/src/agents/pi/__tests__/pi-auto-review-policy.test.ts index 14309a388b..3158ecfd9c 100644 --- a/packages/maker-core/src/agents/pi/__tests__/pi-auto-review-policy.test.ts +++ b/packages/maker-core/src/agents/pi/__tests__/pi-auto-review-policy.test.ts @@ -64,6 +64,18 @@ describe('classifyPiToolForAutoReview', () => { expect(verdict('bash', {})).not.toBe('auto-approve'); }); + it('routes Pi 0.84.3 powershell through the same shell classifier, not unknown-tool gray', () => { + expect(verdict('powershell', { command: 'git status' })).toBe('auto-approve'); + expect(verdict('powershell', { command: 'sudo whoami' })).toBe('prompt-each-time'); + expect(verdict('powershell', { command: 'rm -rf /' })).toBe('prompt-each-time'); + expect(verdict('powershell', {})).not.toBe('auto-approve'); + expect(verdict( + 'powershell', + { command: 'Get-Content innocent.txt' }, + ['/Users/t/.ssh/id_rsa'], + )).toBe('prompt-each-time'); + }); + it('approves plain reads but always prompts for credential paths (bridge-drift defense)', () => { expect(verdict('read', { path: `${WS}/src/a.ts` })).toBe('auto-approve'); expect(verdict('read', { path: '/Users/t/.ssh/id_rsa' })).toBe('prompt-each-time'); diff --git a/packages/maker-core/src/agents/pi/__tests__/pi-translator.test.ts b/packages/maker-core/src/agents/pi/__tests__/pi-translator.test.ts index 6fe6b7b32c..14155b52da 100644 --- a/packages/maker-core/src/agents/pi/__tests__/pi-translator.test.ts +++ b/packages/maker-core/src/agents/pi/__tests__/pi-translator.test.ts @@ -1010,6 +1010,23 @@ describe('pi translator', () => { expect(full).toMatchObject({ data: { fullText: stale } }); }); + it('ignores Pi 0.84.3 session_compact_failed RPC leaks without hanging compaction UI', () => { + const ctx = createPiTranslateContext(noopLogger); + const { queue, events } = makeQueue(); + translatePiEvent( + ev({ + type: 'session_compact_failed', + reason: 'overflow', + aborted: false, + errorMessage: 'quota', + }), + queue, + ctx, + ); + expect(events.some((event) => event.type === 'compact_boundary')).toBe(false); + expect(events.some((event) => event.type === 'error')).toBe(false); + }); + it('maps compaction_end (threshold) → compact_boundary with token deltas + updates contextTokens', () => { const ctx = createPiTranslateContext(noopLogger); const { queue, events } = makeQueue(); diff --git a/packages/maker-core/src/agents/pi/auto-review-policy.ts b/packages/maker-core/src/agents/pi/auto-review-policy.ts index 03d5cfd934..2430abf684 100644 --- a/packages/maker-core/src/agents/pi/auto-review-policy.ts +++ b/packages/maker-core/src/agents/pi/auto-review-policy.ts @@ -5,7 +5,7 @@ * * pi 侧的到达面与 CC 不同:bridge 在 pi 进程内直通「只读内置四件套且入参不碰凭证 * 路径」(见 cindy-bridge-source.ts READONLY_BUILTINS + touchesCredentialPath), - * `bypassPermissions` 全放行 —— 能到这里的是 bash / edit / write / 桥接 MCP 工具 / + * `bypassPermissions` 全放行 —— 能到这里的是 bash / powershell / edit / write / 桥接 MCP 工具 / * 凭证路径的只读调用 / 未来新增内置工具。只读分支扫全部字符串入参判凭证,与 bridge * 同判定:bridge 升级上来的凭证读在 auto 档必须落弹窗,不能被 path 字段缺失反向放行。 * @@ -65,6 +65,9 @@ const READ_ONLY_TOOLS: ReadonlySet = new Set(['read', 'grep', 'find', 'l /** 会改文件、带结构化 `path` 入参的 pi 内置工具。 */ const FILE_WRITE_TOOLS: ReadonlySet = new Set(['write', 'edit']); +/** Pi v0.84.3 起 Windows 可选 powershell 与 bash 同为 shell 执行面,入参都是 `command`。 */ +const SHELL_TOOLS: ReadonlySet = new Set(['bash', 'powershell']); + function stringField(input: Record, key: string): string | undefined { const v = input[key]; return typeof v === 'string' && v.length > 0 ? v : undefined; @@ -141,7 +144,7 @@ export function normalizePiToolForAutoReview(ctx: PiAutoReviewContext): Reviewab if (FILE_WRITE_TOOLS.has(toolName)) { return { kind: 'file-write', path: stringField(input, 'path') }; } - if (toolName === 'bash') { + if (SHELL_TOOLS.has(toolName)) { const evidenceAction = canonicalCredentialEvidenceAction(ctx.resolvedCredentialPaths); if (evidenceAction) return evidenceAction; return { kind: 'exec', command: stringField(input, 'command') ?? '' }; diff --git a/packages/maker-core/src/agents/pi/cindy-bridge-source.ts b/packages/maker-core/src/agents/pi/cindy-bridge-source.ts index b21fe84723..e85bc1da5c 100644 --- a/packages/maker-core/src/agents/pi/cindy-bridge-source.ts +++ b/packages/maker-core/src/agents/pi/cindy-bridge-source.ts @@ -68,6 +68,7 @@ import { createGrepTool, createLsTool, } from '@earendil-works/pi-coding-agent'; +import * as piCodingAgent from '@earendil-works/pi-coding-agent'; const PERMISSION_TITLE = 'cindy:permission'; const TURN_CHANGE_CAPTURE_TITLE = 'cindy:turn-change-capture'; @@ -76,6 +77,9 @@ const PERMISSION_USER_DENY = 'user-deny'; const PERMISSION_AUTO_REVIEW_DENY = 'auto-review-deny'; const READONLY_BUILTINS = new Set(['read', 'grep', 'find', 'ls']); const FILE_WRITE_BUILTINS = new Set(['edit', 'write']); +function isCindyShellTool(toolName: unknown): boolean { + return toolName === 'bash' || toolName === 'powershell'; +} const MANAGED_RG_PATH_ENV = 'CINDY_PI_MANAGED_RG_PATH'; const SUBAGENT_RUN_DIR_ENV = 'CINDY_PI_SUBAGENT_RUN_DIR'; const PI_PACKAGE_MANAGEMENT_ENV = 'CINDY_PI_PACKAGE_MANAGEMENT'; @@ -730,6 +734,14 @@ function bashExpandedPathCandidates( } } +function powershellInputReadEvidence(input: unknown): BashInputReadEvidence { + if (!input || typeof input !== 'object') return { targets: [], unresolved: false }; + const command = (input as Record).command; + if (typeof command !== 'string' || !command) return { targets: [], unresolved: false }; + // PowerShell 不走 bash 重定向解析器;整条命令当字符串叶子扫凭证特征。 + return { targets: [command], unresolved: false }; +} + function bashInputReadEvidence(input: unknown): BashInputReadEvidence { if (!input || typeof input !== 'object') return { targets: [], unresolved: false }; const command = (input as Record).command; @@ -2911,6 +2923,52 @@ export default async function cindyBridge(pi: any) { }, }); + // Pi v0.84.3 Windows powershell is the same spawn family as bash. Overlay only + // when the runtime exports the factory so 0.83.0 sessions keep loading. + const createPowerShellTool = (piCodingAgent as { createPowerShellTool?: typeof createBashTool }).createPowerShellTool; + if (typeof createPowerShellTool === 'function') { + const powershellTool = createPowerShellTool(process.cwd(), { + exposeSessionEnvironment: false, + spawnHook: ({ command, cwd, env }) => ({ + command, + cwd, + env: isolatedBashEnvironment(env, bashPackageHome), + }), + }); + var powershellParameters = powershellTool.parameters; + if ( + powershellParameters && + typeof powershellParameters === 'object' && + powershellParameters.properties && + typeof powershellParameters.properties === 'object' && + powershellParameters.properties.timeout && + typeof powershellParameters.properties.timeout === 'object' + ) { + powershellParameters.properties.timeout.description = cindyBashTimeoutDescription(); + } + if (typeof powershellTool.description === 'string') { + powershellTool.description = + powershellTool.description + + ' Cindy enforces a ' + + CINDY_PI_BASH_DEFAULT_TIMEOUT_SECONDS + + 's default and a ' + + CINDY_PI_BASH_MAX_TIMEOUT_SECONDS + + 's maximum.'; + } + pi.registerTool({ + ...powershellTool, + execute: async (id: string, params: unknown, signal: AbortSignal, onUpdate: unknown) => { + const nextParams = applyCindyBashTimeoutParams(params); + if (bashCommandMutatesPiPackages(nextParams)) { + throw new Error( + 'Direct Pi extension changes are unavailable through bash. Use cindy_pi_extension so Cindy can request confirmation.', + ); + } + return powershellTool.execute(id, nextParams as any, signal, onUpdate as any); + }, + }); + } + // Cindy owns a separate Pi extension store. Directly running the bundled Pi // CLI from bash writes to Pi's default user home and bypasses Cindy's // compatibility/approval state. Normal local tasks therefore receive one @@ -3128,7 +3186,7 @@ export default async function cindyBridge(pi: any) { // bash 读取任意进程的初始环境(/proc//environ)是绕过密钥剥离的旁路: // spawn 边界虽删了子进程 env 的私密变量,父 pi 进程仍持有,cat /proc/PPID/environ // 同 UID 直取代理 token / 网关 / BYOM key(codex 报)→ 一律硬拦,含 Full access。 - if (event.toolName === 'bash' && commandReadsProcessEnviron(event.input?.command)) { + if (isCindyShellTool(event.toolName) && commandReadsProcessEnviron(event.input?.command)) { return { block: true, reason: 'Cindy blocks reading process environment (/proc/*/environ), even with Full access.' }; } // 凭证/密钥路径的内置只读工具与 bash 输入重定向都必须携带 canonical @@ -3136,18 +3194,20 @@ export default async function cindyBridge(pi: any) { // parser 提取真实输入目标,才能识别工作区 symlink 指向的凭证文件。 const bashReadEvidence = event.toolName === 'bash' ? bashInputReadEvidence(event.input) - : { targets: [], unresolved: false }; + : event.toolName === 'powershell' + ? powershellInputReadEvidence(event.input) + : { targets: [], unresolved: false }; const bashReadTargets = bashReadEvidence.targets; const readonlyCredentialEvidence = READONLY_BUILTINS.has(event.toolName) ? collectReadonlyCredentialEvidence(event.toolName, event.input) : null; const resolvedCredentialReadPaths = readonlyCredentialEvidence ? [...new Set(collectResolvedCredentialPaths(readonlyCredentialEvidence.paths))] - : event.toolName === 'bash' + : isCindyShellTool(event.toolName) ? [...new Set(collectResolvedCredentialPaths(bashReadTargets))] : []; const credentialRead = readonlyCredentialEvidence?.touchesCredential === true - || (event.toolName === 'bash' && (bashReadEvidence.unresolved || touchesCredentialPath(bashReadTargets))) + || (isCindyShellTool(event.toolName) && (bashReadEvidence.unresolved || touchesCredentialPath(bashReadTargets))) || resolvedCredentialReadPaths.length > 0; const credentialEvidenceForHost = resolvedCredentialEvidenceForHost( resolvedCredentialReadPaths, diff --git a/packages/maker-core/src/agents/pi/translator.ts b/packages/maker-core/src/agents/pi/translator.ts index e4993e8204..dce68d8df9 100644 --- a/packages/maker-core/src/agents/pi/translator.ts +++ b/packages/maker-core/src/agents/pi/translator.ts @@ -1086,6 +1086,9 @@ export function translatePiEvent( case 'summarization_retry_attempt_start': case 'summarization_retry_finished': case 'bash_execution_update': + // Pi v0.84.3 extension telemetry. If it ever leaks onto the RPC stream, + // ignore it: compaction_end already carries aborted/errorMessage. + case 'session_compact_failed': return; case 'extension_error': {