Skip to content

fix(local-agent): derive deterministic local_agent_id + harden install path - #161

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
jeff-r2026:fix/local-agent-id-deterministic
Jul 8, 2026
Merged

fix(local-agent): derive deterministic local_agent_id + harden install path#161
jeff-r2026 merged 1 commit into
Tencent:mainfrom
jeff-r2026:fix/local-agent-id-deterministic

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Problem

The HTTP local-agent generated local_agent_id via crypto.randomBytes(8), which is non-deterministic:

  • env-only path (loadLocalAgentConfig with only TEAMAI_HTTP_ENDPOINT set): a fresh random id was minted on every hook fire and never persisted — so session_start / prompt_submit / each tool-use reported a different id.
  • init --http path: random once then persisted, but a new random id on every reinstall (config removed) or on a different machine.

Either way the backend saw a churn of unrelated random agents instead of one stable agent per machine/tool. This regressed the design contract already documented in src/machine-id.ts: local_agent_id = sha1(agent_type + machine_id + path_hash)[:16], stable for the same machine + install dir + agent_type.

Fix

Reuse the existing deterministic deriveLocalAgentId(agentType, machineId, installPath):

  • New resolveLocalAgentId(context) derives the id at runtime. The tool is auto-detected from the hook's --tool flag (context.tool), so claude / codebuddy / workbuddy each get a stable, distinct id.
  • localAgentId is no longer stored in config.json (field kept optional for backward-compatible loads of old config files).
  • TEAMAI_LOCAL_AGENT_ID still overrides for explicit pinning.

Security hardening (install/sync command path)

Backend sync commands are untrusted input; three issues flagged by an automated security review, all pre-existing in the HTTP local-agent module, fixed here since they live in the same file:

  • Path traversalvalidateSlug() in commandSlug() rejects slugs containing /, \, .., or absolute paths before they reach path.join.
  • SSRF / arbitrary local file readdownloadResource() drops the file:// and absolute-local-path copy branches, allows only http(s) schemes, and follows redirects manually so each hop's scheme is re-validated. (Kept http allowed so internal/self-hosted endpoints keep working; no private-IP block by design.)
  • Insecure credential permswriteTokenFile() writes the token with owner-only 0o600 and chmods an already-existing token file to tighten it.

Test Plan

  • npx tsc --noEmit
  • npx vitest run src/__tests__/local-agent.test.ts src/__tests__/machine-id.test.ts — 16 passed (incl. 4 new security tests)
  • npm run build
  • E2E (id-fix): real CLI hook-dispatch with --tool claude|codebuddy|workbuddy against a mock server — same tool → same id across calls; different tools → different ids; report & sync ids match; TEAMAI_LOCAL_AGENT_ID override honoured.
  • E2E (security): real CLI hook-dispatch where the mock server returns malicious commands — slug ../../evil → ack failed: Invalid resource slug, no file escapes the repo; file:///etc/passwd download_url → ack failed: Unsupported download URL scheme: file:.

🤖 Generated with Claude Code

…l path

The HTTP local-agent generated local_agent_id via crypto.randomBytes, so the
id changed on every hook fire (env-only path) or per reinstall/machine — the
backend saw a fresh random agent each time instead of a stable one. Reuse the
existing deterministic deriveLocalAgentId(agentType, machineId, installPath):
the tool is auto-detected from the hook --tool flag, so claude/codebuddy/
workbuddy each get a stable, distinct id. localAgentId is no longer stored in
config.json (kept optional for backward-compatible loads).

Also harden the install/sync command path (backend commands are untrusted):
- validateSlug() in commandSlug() rejects path-traversal slugs (../, /, \, abs)
- downloadResource() drops file:// / local-path copy, allows only http(s), and
  follows redirects manually so each hop's scheme is re-validated
- writeTokenFile() writes the credential file with owner-only 0o600 perms

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeff-r2026
jeff-r2026 merged commit f3c61a4 into Tencent:main Jul 8, 2026
6 checks passed
@jeff-r2026
jeff-r2026 deleted the fix/local-agent-id-deterministic branch July 8, 2026 03:12
jeff-r2026 added a commit that referenced this pull request Jul 8, 2026
…r slug (#162)

* fix(local-agent): name HTTP-synced skills by SKILL.md name, not server slug

When skills are delivered over the HTTP local-agent path, the directory was
named after the server-provided skill_slug. But the SKILL.md `name:` frontmatter
is what the AI tool uses to identify a skill, and the two can differ — so a
skill synced as slug "skillsaaa" landed at ~/.codebuddy/skills/skillsaaa and was
unrecognizable. The git-native path already fixed this in skill-command.ts
(#144); this brings the HTTP path (local-agent.ts) in line.

- resolveSkillDirName() reads the SKILL.md name after extraction and uses it as
  the on-disk directory name when it differs from the slug, passing path-safety
  validation; falls back to the slug when the name is missing/equal/unsafe.
- The manifest records dir_name (keyed by slug) so uninstall_skill can locate
  and remove the SKILL.md-name directory when the backend only knows the slug.

Not a regression from #161 — verified by building the prior internal release and
reproducing identical slug-based naming; this is a pre-existing gap in the HTTP
path since it was introduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(local-agent): wrap zip body in Buffer for BodyInit type compat

CI's @types/node types `zipSync`'s return as `Uint8Array<ArrayBufferLike>`,
which is not assignable to `BodyInit` in `new Response(...)`. Wrap it in
`Buffer.from()` (a valid BodyInit) so `tsc --noEmit` passes across type versions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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