Skip to content

feat(sam-node): add skill install, --daemonize, and reset --all - #268

Merged
aojea merged 1 commit into
google:mainfrom
aojea:ux_skill
Aug 13, 2026
Merged

feat(sam-node): add skill install, --daemonize, and reset --all#268
aojea merged 1 commit into
google:mainfrom
aojea:ux_skill

Conversation

@aojea

@aojea aojea commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Getting an AI agent onto the mesh took a page of manual steps: paste the skill in by hand, invent an API token, keep a terminal pinned to a foreground node, and copy MCP config out of the docs.

  • sam-node skill [install|list|show] installs the embedded SAM agent skill into the directories Claude Code and Antigravity scan, so an agent knows when and how to drive the mesh on its own. SKILL.md now also covers bootstrapping a node, and reaching mesh inference through the OpenAI-compatible facade rather than call_remote_tool.
  • sam-node run --daemonize starts the node detached and returns once its local API answers, generating an API token in the data directory when none is configured. A failed startup is reported with the child's log, and the command is idempotent.
  • sam-node reset --all deletes every file the node keeps, so a broken or half-configured setup can be started over instead of silently inheriting old state. It confirms first, and needs --yes where there is no terminal to confirm on.

internal/node: NewStore wraps its file-lock timeout in an ErrStoreLocked sentinel, which is how the CLI tells "a node is already running" apart from a genuine store failure.

Getting an AI agent onto the mesh took a page of manual steps: paste the
skill in by hand, invent an API token, keep a terminal pinned to a
foreground node, and copy MCP config out of the docs.

- `sam-node skill [install|list|show]` installs the embedded SAM agent
  skill into the directories Claude Code and Antigravity scan, so an
  agent knows when and how to drive the mesh on its own. SKILL.md now
  also covers bootstrapping a node, and reaching mesh inference through
  the OpenAI-compatible facade rather than call_remote_tool.
- `sam-node run --daemonize` starts the node detached and returns once
  its local API answers, generating an API token in the data directory
  when none is configured. A failed startup is reported with the child's
  log, and the command is idempotent.
- `sam-node reset --all` deletes every file the node keeps, so a broken
  or half-configured setup can be started over instead of silently
  inheriting old state. It confirms first, and needs --yes where there
  is no terminal to confirm on.

internal/node: NewStore wraps its file-lock timeout in an ErrStoreLocked
sentinel, which is how the CLI tells "a node is already running" apart
from a genuine store failure.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces background daemonization for sam-node via the --daemonize flag, adds a reset --all command to completely purge local node state, and implements a skill command to install and manage the SAM agent skill for local AI coding assistants. The review feedback highlights three key areas for improvement: killing the background child process if startup verification fails to prevent process leaks, robustly handling empty or corrupt token files during automatic token generation, and ensuring the E2E tests are portable across Linux and macOS by avoiding GNU-specific stat flags.

Comment thread cmd/sam-node/daemonize.go
Comment on lines +105 to +107
if err := waitForDaemon(probe, exited); err != nil {
return fmt.Errorf("%w\nLast lines of %s:\n%s", err, logPath, tailFile(logPath, 8192, 10))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If waitForDaemon times out or fails, the child process might still be running in an unresponsive or stuck state. Since the parent process exits with an error, this child process is leaked in the background, which will keep the database locked and prevent subsequent startup attempts.

Explicitly killing the child process on startup failure ensures clean resource cleanup.

Suggested change
if err := waitForDaemon(probe, exited); err != nil {
return fmt.Errorf("%w\nLast lines of %s:\n%s", err, logPath, tailFile(logPath, 8192, 10))
}
if err := waitForDaemon(probe, exited); err != nil {
_ = child.Process.Kill()
return fmt.Errorf("%w\nLast lines of %s:\n%s", err, logPath, tailFile(logPath, 8192, 10))
}

Comment thread cmd/sam-node/daemonize.go
Comment on lines +140 to +147
if _, err := os.Stat(tokenPath); err == nil {
return []string{"--api-token-path", tokenPath}, tokenPath, nil
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return nil, "", fmt.Errorf("generating an API token: %w", err)
}
f, err := os.OpenFile(tokenPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If tokenPath already exists but is empty (e.g., due to an interrupted write or crash in a previous run), os.Stat will succeed, and the daemon will be started with an empty token file, which might bypass authentication or cause unexpected behavior.

Furthermore, if we only check for non-empty files but keep os.O_EXCL, the subsequent os.OpenFile call will fail because the file already exists.

We should ensure the file is non-empty, and use os.O_TRUNC instead of os.O_EXCL so we can safely overwrite an empty or corrupt token file.

Suggested change
if _, err := os.Stat(tokenPath); err == nil {
return []string{"--api-token-path", tokenPath}, tokenPath, nil
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return nil, "", fmt.Errorf("generating an API token: %w", err)
}
f, err := os.OpenFile(tokenPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
if info, err := os.Stat(tokenPath); err == nil && info.Size() > 0 {
return []string{"--api-token-path", tokenPath}, tokenPath, nil
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return nil, "", fmt.Errorf("generating an API token: %w", err)
}
f, err := os.OpenFile(tokenPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)

Comment thread tests/e2e/sam.bats
# The child ran detached with a generated token and its own log.
[[ -f "$XDG_CONFIG_HOME/sam-mesh/api-token" ]]
[[ -s "$XDG_CONFIG_HOME/sam-mesh/sam-node.log" ]]
[[ "$(stat -c %a "$XDG_CONFIG_HOME/sam-mesh/api-token")" == "600" ]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The stat -c %a command is specific to GNU stat (Linux) and will fail on macOS/BSD systems where stat -f %Lp is used instead. This will break E2E tests for developers running on macOS.

Using a portable fallback ensures the tests run seamlessly across both Linux and macOS environments.

  [[ "$(stat -c %a "$XDG_CONFIG_HOME/sam-mesh/api-token" 2>/dev/null || stat -f %Lp "$XDG_CONFIG_HOME/sam-mesh/api-token")" == "600" ]]

@aojea
aojea merged commit 987eaaa into google:main Aug 13, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant