mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-25 17:36:43 +08:00
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:
1 parent
d743b147b8
commit
1c03a59376
4 files changed
+109
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user