feat(pi): inject permission mode into agent process env (#1637)

* feat(pi): inject permission mode into agent process env

The pi permission-gate extension runs inside the pi CLI and has no way
to learn cc-connect's permission mode. When a user switches to yolo
(auto-approve) mode in Feishu, the engine restarts the session, but the
new pi process is still spawned without any mode hint, so the extension
keeps emitting permission cards.

Inject CC_PERMISSION_MODE=<mode> into the spawned pi process environment
in StartSession. The extension reads it and auto-approves all tool calls
in yolo mode. Mode switches already recreate the session (pi does not
implement LiveModeSwitcher), so the new process picks up the new value.

* test(pi): check Close error in StartSession test to satisfy errcheck

* refactor(pi): move permission-mode env injection into core helper

Extract the hardcoded CC_PERMISSION_MODE append into core.InjectedAgentEnv
so the env var becomes a documented, reusable part of cc-connect's CC_*
extension contract instead of a one-off string in the pi agent. Other
agents can opt into the same convention.

Add TestAgent_StartSession_UserOverrideWins to lock in that the
engine-injected value is appended after config/session env, so a
user-supplied CC_PERMISSION_MODE keeps priority (getenv returns the first
match).
This commit is contained in:
baizhu945 authored and GitHub committed 2026-08-14 08:31:08 +08:00
1 parent d743b147b8
commit 1c03a59376
4 files changed
+109

No files matched your search

+6
View File
@@ -148,6 +148,12 @@ func (a *Agent) StartSession(ctx context.Context, sessionID string) (core.AgentS
extraArgs := append([]string{}, a.cliExtraArgs...)
extraEnv := append([]string(nil), a.configEnv...)
extraEnv = append(extraEnv, a.sessionEnv...)
// 注入权限模式环境变量,供 permission-gate 扩展读取:yolo(全自动)时扩展
// 自动放行所有工具,不再弹出权限确认卡片。模式切换会触发会话重建(pi 未实现
// LiveModeSwitcher),新进程拿到新值。core.InjectedAgentEnv 追加在
// configEnv/sessionEnv 之后,因此用户显式设置的 CC_PERMISSION_MODE 排在前、
// 优先生效(getenv 返回第一个匹配项)。
extraEnv = append(extraEnv, core.InjectedAgentEnv(mode)...)
rpc := a.rpc
a.mu.Unlock()
return newPiSession(ctx, a.cmd, extraArgs, a.workDir, model, mode, thinking, rpc, sessionID, extraEnv)
+67
View File
@@ -571,6 +571,73 @@ func TestAgent_StartSession(t *testing.T) {
if !ps.Alive() {
t.Error("session should be alive")
}
// CC_PERMISSION_MODE 必须被注入,permission-gate 扩展才能感知全自动模式。
found := false
for _, e := range ps.extraEnv {
if e == "CC_PERMISSION_MODE=yolo" {
found = true
break
}
}
if !found {
t.Errorf("extraEnv = %v, want CC_PERMISSION_MODE=yolo", ps.extraEnv)
}
}
func TestAgent_StartSession_NoModeNoEnv(t *testing.T) {
a := &Agent{cmd: "echo", workDir: "/tmp"} // mode 为空
sess, err := a.StartSession(context.Background(), "")
if err != nil {
t.Fatalf("StartSession() error = %v", err)
}
defer func() {
if err := sess.Close(); err != nil {
t.Errorf("Close() error = %v", err)
}
}()
ps := sess.(*piSession)
for _, e := range ps.extraEnv {
if len(e) >= len("CC_PERMISSION_MODE=") && e[:len("CC_PERMISSION_MODE=")] == "CC_PERMISSION_MODE=" {
t.Errorf("extraEnv = %v, CC_PERMISSION_MODE should be absent when mode is empty", ps.extraEnv)
}
}
}
func TestAgent_StartSession_UserOverrideWins(t *testing.T) {
// 回归保护:引擎注入的 CC_PERMISSION_MODE 必须追加在 configEnv/sessionEnv
// 之后,这样用户显式设置的 CC_PERMISSION_MODE 排在前面、优先生效(getenv
// 返回第一个匹配项)。若未来重构把它提前,引擎值会反过来覆盖用户的显式设置。
a := &Agent{cmd: "echo", workDir: "/tmp", mode: "yolo"}
a.SetSessionEnv([]string{"CC_PERMISSION_MODE=default"})
sess, err := a.StartSession(context.Background(), "")
if err != nil {
t.Fatalf("StartSession() error = %v", err)
}
defer func() {
if err := sess.Close(); err != nil {
t.Errorf("Close() error = %v", err)
}
}()
ps := sess.(*piSession)
userIdx, engineIdx := -1, -1
for i, e := range ps.extraEnv {
switch e {
case "CC_PERMISSION_MODE=default":
userIdx = i
case "CC_PERMISSION_MODE=yolo":
engineIdx = i
}
}
if userIdx == -1 || engineIdx == -1 {
t.Fatalf("extraEnv = %v, want both user (default) and engine (yolo) CC_PERMISSION_MODE entries", ps.extraEnv)
}
if userIdx > engineIdx {
t.Errorf("user CC_PERMISSION_MODE at %d must precede engine value at %d so user override wins", userIdx, engineIdx)
}
}
// ── extractToolInput ─────────────────────────────────────────
+23
View File
@@ -34,6 +34,29 @@ func MergeEnv(base, extra []string) []string {
return append(merged, extra...)
}
// InjectedAgentEnv returns the env vars cc-connect injects into a spawned
// agent process so in-process extensions can learn cc-connect's runtime state.
// The CC_ prefix marks these vars as cc-connect's public extension contract,
// alongside CC_PROJECT / CC_SESSION_KEY / CC_DATA_DIR that the engine injects
// as session env.
//
// Currently only the permission mode is exposed:
//
// CC_PERMISSION_MODE — the session's permission mode ("default" | "yolo").
// Extensions such as the pi permission-gate read it to auto-approve tool
// calls in yolo mode. An empty mode returns nil, so non-yolo sessions see
// no injected var.
//
// Kept as a single core helper so every agent opts into the same convention
// instead of hardcoding the variable name; extending the contract (e.g.
// CC_MODEL, CC_THINKING) only means extending this function.
func InjectedAgentEnv(mode string) []string {
if mode == "" {
return nil
}
return []string{"CC_PERMISSION_MODE=" + mode}
}
// CheckAllowFrom logs a security warning at startup when allow_from is not
// configured (defaults to permit-all). Platforms should call this during init.
func CheckAllowFrom(platform, allowFrom string) {
+13
View File
@@ -49,6 +49,19 @@ func TestUnauthorizedAccessMessage(t *testing.T) {
}
}
func TestInjectedAgentEnv(t *testing.T) {
// Empty mode must yield no injected var (do-no-harm for non-yolo sessions).
if got := InjectedAgentEnv(""); got != nil {
t.Fatalf("InjectedAgentEnv(\"\") = %v, want nil", got)
}
// Non-empty mode must produce the single CC_PERMISSION_MODE entry.
got := InjectedAgentEnv("yolo")
if len(got) != 1 || got[0] != "CC_PERMISSION_MODE=yolo" {
t.Fatalf("InjectedAgentEnv(\"yolo\") = %v, want [CC_PERMISSION_MODE=yolo]", got)
}
}
// TestSaveFilesToDisk_RejectsPathTraversal is a regression test for a real
// path-traversal vulnerability in SaveFilesToDisk: the attachment FileName
// (which comes from user-controlled IM/HTTP upload metadata) was passed