Sync MCP surface to SPEC §7 process model

Rename list_children/read_output/kill/send_message_to to their SPEC §7
process_id-shaped names; drop report_to_parent (direction inferred by
send_message) and policy_check (replaced by per-project trust gating).
Add the SPEC's missing tools: start_process, restart_process,
close_process, rename_process, select_process, get_process_status,
get_project_status, get_process_raw_output, search_output,
get_process_ports, whoami, help.

Process model now distinguishes agent/terminal/command kinds with
opaque p_<6hex> IDs. Command entries are session-persistent so they
survive PTY exit and can be Restart'd. Status enum gains starting and
stopped. screen_version, port detection, and bracketed-paste send_input
land alongside.

Trust gating (internal/trust) replaces the regex policy: command-preset
spawns return needs_trust on first use; the user confirms in a
status-line modal and the grant persists to
\$XDG_DATA_HOME/patterm/projects/<key>/trust.json.

Tests cover send_message direction inference (parent↔child, sibling
rejection, nil caller paths) and trust grant persistence across reopen.
This commit is contained in:
2026-05-14 14:29:45 +01:00
parent 2852c48e60
commit 55c6c93086
14 changed files with 2316 additions and 664 deletions

View File

@@ -90,7 +90,15 @@ func (l *Launcher) LaunchAgent(p *preset.Preset, displayName, initialPrompt, par
// Spawn with the chosen identity.
cols, rows := l.size()
c, err := l.sess.spawnWithIdentity(displayName, KindAgent, argv, env, cols, rows, parentID, identity)
c, err := l.sess.Spawn(SpawnSpec{
Kind: KindAgent,
Argv: argv,
Env: env,
Name: displayName,
ParentID: parentID,
PresetRef: p.Name,
Identity: identity,
}, cols, rows)
if err != nil {
_ = os.Remove(mcpConfigPath)
return nil, err
@@ -111,17 +119,72 @@ func (l *Launcher) LaunchAgent(p *preset.Preset, displayName, initialPrompt, par
return c, nil
}
// LaunchProcess spawns a process preset. No MCP injection; just argv.
func (l *Launcher) LaunchProcess(p *preset.Preset, displayName string) (*Child, error) {
if p.Kind != preset.KindProcess {
return nil, fmt.Errorf("launch: %q is not a process preset", p.Name)
// LaunchCommandPreset spawns a process preset as a SPEC §7 command
// entry. No MCP injection; just argv. The entry is session-persistent
// (survives PTY exit so it can be Restart'd).
func (l *Launcher) LaunchCommandPreset(p *preset.Preset, displayName, parentID string) (*Child, error) {
if p.Kind != preset.KindCommand {
return nil, fmt.Errorf("launch: %q is not a command preset", p.Name)
}
env := l.sess.ChildEnv()
for k, v := range p.Env {
env = append(env, k+"="+v)
}
cols, rows := l.size()
return l.sess.Spawn(displayName, KindProcess, p.ResolvedArgv(), env, cols, rows, "")
return l.sess.Spawn(SpawnSpec{
Kind: KindCommand,
Argv: p.ResolvedArgv(),
Env: env,
Name: displayName,
ParentID: parentID,
WorkDir: p.WorkingDir,
PresetRef: p.Name,
}, cols, rows)
}
// LaunchCommandArgv spawns a freeform-argv command entry. Trust gating
// (SPEC §7) lives one level up in toolHost — by the time we get here
// trust is settled (freeform argv is implicitly trusted).
func (l *Launcher) LaunchCommandArgv(argv []string, displayName, parentID, workDir string, env []string, shell bool) (*Child, error) {
if shell && len(argv) > 0 {
argv = []string{"sh", "-lc", strings.Join(argv, " ")}
}
if env == nil {
env = l.sess.ChildEnv()
}
cols, rows := l.size()
return l.sess.Spawn(SpawnSpec{
Kind: KindCommand,
Argv: argv,
Env: env,
Name: displayName,
ParentID: parentID,
WorkDir: workDir,
}, cols, rows)
}
// LaunchTerminal spawns a bare interactive shell. SPEC §7 kind=terminal.
// argv defaults to $SHELL -i when empty.
func (l *Launcher) LaunchTerminal(argv []string, displayName, parentID, workDir string, env []string) (*Child, error) {
if len(argv) == 0 {
sh := os.Getenv("SHELL")
if sh == "" {
sh = "/bin/sh"
}
argv = []string{sh, "-i"}
}
if env == nil {
env = l.sess.ChildEnv()
}
cols, rows := l.size()
return l.sess.Spawn(SpawnSpec{
Kind: KindTerminal,
Argv: argv,
Env: env,
Name: displayName,
ParentID: parentID,
WorkDir: workDir,
}, cols, rows)
}
func (l *Launcher) writeMCPConfig() (identity, path string, err error) {