This commit is contained in:
Anton Bogdanovich 2026-05-15 11:29:45 +03:00 committed by GitHub
commit 9ce35ac908
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 76 additions and 0 deletions

View file

@ -236,6 +236,13 @@ func (al *AgentLoop) allocateRouteSession(route routing.ResolvedRoute, msg bus.I
})
}
func originTopicID(origin *bus.InboundContext) string {
if origin == nil {
return ""
}
return strings.TrimSpace(origin.TopicID)
}
func (al *AgentLoop) processSystemMessage(
ctx context.Context,
msg bus.InboundMessage,

View file

@ -522,6 +522,7 @@ toolLoop:
ts.opts.Dispatch.MessageID(),
ts.opts.Dispatch.ReplyToMessageID(),
)
execCtx = tools.WithToolTopicID(execCtx, originTopicID(ts.opts.Dispatch.InboundContext))
execCtx = tools.WithToolSessionContext(
execCtx,
ts.agent.ID,

View file

@ -45,6 +45,7 @@ type toolCtxKey struct{ name string }
var (
ctxKeyChannel = &toolCtxKey{"channel"}
ctxKeyChatID = &toolCtxKey{"chatID"}
ctxKeyTopicID = &toolCtxKey{"topicID"}
ctxKeyMessageID = &toolCtxKey{"messageID"}
ctxKeyReplyToMessageID = &toolCtxKey{"replyToMessageID"}
ctxKeyAgentID = &toolCtxKey{"agentID"}
@ -59,6 +60,12 @@ func WithToolContext(ctx context.Context, channel, chatID string) context.Contex
return ctx
}
// WithToolTopicID returns a child context carrying the inbound topic/thread id.
func WithToolTopicID(ctx context.Context, topicID string) context.Context {
ctx = context.WithValue(ctx, ctxKeyTopicID, topicID)
return ctx
}
// WithToolMessageContext returns a child context carrying inbound message IDs.
func WithToolMessageContext(ctx context.Context, messageID, replyToMessageID string) context.Context {
ctx = context.WithValue(ctx, ctxKeyMessageID, messageID)
@ -100,6 +107,12 @@ func ToolChatID(ctx context.Context) string {
return v
}
// ToolTopicID extracts the inbound topic/thread id from ctx, or "" if unset.
func ToolTopicID(ctx context.Context) string {
v, _ := ctx.Value(ctxKeyTopicID).(string)
return v
}
// ToolMessageID extracts the current inbound message ID from ctx, or "" if unset.
func ToolMessageID(ctx context.Context) string {
v, _ := ctx.Value(ctxKeyMessageID).(string)

View file

@ -42,6 +42,10 @@ func WithToolContext(ctx context.Context, channel, chatID string) context.Contex
return toolshared.WithToolContext(ctx, channel, chatID)
}
func WithToolTopicID(ctx context.Context, topicID string) context.Context {
return toolshared.WithToolTopicID(ctx, topicID)
}
func WithToolMessageContext(ctx context.Context, messageID, replyToMessageID string) context.Context {
return toolshared.WithToolMessageContext(ctx, messageID, replyToMessageID)
}
@ -69,6 +73,10 @@ func ToolChatID(ctx context.Context) string {
return toolshared.ToolChatID(ctx)
}
func ToolTopicID(ctx context.Context) string {
return toolshared.ToolTopicID(ctx)
}
func ToolMessageID(ctx context.Context) string {
return toolshared.ToolMessageID(ctx)
}

View file

@ -372,6 +372,13 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult
if cwd != "" {
cmd.Dir = cwd
}
cmd.Env = append(os.Environ(),
"PICOCLAW_TOOL_CHANNEL="+ToolChannel(ctx),
"PICOCLAW_TOOL_CHAT_ID="+ToolChatID(ctx),
"PICOCLAW_TOOL_TOPIC_ID="+ToolTopicID(ctx),
"PICOCLAW_TOOL_MESSAGE_ID="+ToolMessageID(ctx),
"PICOCLAW_TOOL_REPLY_TO_MESSAGE_ID="+ToolReplyToMessageID(ctx),
)
prepareCommandForTermination(cmd)
@ -484,6 +491,13 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
if cwd != "" {
cmd.Dir = cwd
}
cmd.Env = append(os.Environ(),
"PICOCLAW_TOOL_CHANNEL="+ToolChannel(ctx),
"PICOCLAW_TOOL_CHAT_ID="+ToolChatID(ctx),
"PICOCLAW_TOOL_TOPIC_ID="+ToolTopicID(ctx),
"PICOCLAW_TOOL_MESSAGE_ID="+ToolMessageID(ctx),
"PICOCLAW_TOOL_REPLY_TO_MESSAGE_ID="+ToolReplyToMessageID(ctx),
)
prepareCommandForTermination(cmd)
@ -1073,6 +1087,19 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
for _, loc := range matchIndices {
raw := cmd[loc[0]:loc[1]]
// Skip slash-containing relative command segments like
// "scripts/foo.sh". The absolute-path regex matches the "/foo.sh"
// substring inside that token, but this is not an absolute path.
if strings.HasPrefix(raw, "/") && loc[0] > 0 {
prev := cmd[loc[0]-1]
if (prev >= 'a' && prev <= 'z') ||
(prev >= 'A' && prev <= 'Z') ||
(prev >= '0' && prev <= '9') ||
prev == '_' || prev == '.' || prev == '-' {
continue
}
}
// Skip URL path components that look like they're from web URLs.
// When a URL like "https://github.com" is parsed, the regex captures
// "//github.com" as a match (the path portion after "https:").

View file

@ -703,6 +703,26 @@ func TestShellTool_URLBypassPrevented(t *testing.T) {
}
}
func TestShellTool_RelativeScriptPathNotMisclassifiedAsAbsolute(t *testing.T) {
tmpDir := t.TempDir()
scriptsDir := filepath.Join(tmpDir, "scripts")
require.NoError(t, os.MkdirAll(scriptsDir, 0o755))
scriptPath := filepath.Join(scriptsDir, "echo.sh")
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/bin/sh\necho ok\n"), 0o755))
tool, err := NewExecTool(tmpDir, true)
require.NoError(t, err)
result := tool.Execute(context.Background(), map[string]any{
"action": "run",
"command": "scripts/echo.sh",
"cwd": tmpDir,
})
if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") {
t.Fatalf("relative script path should not be blocked: %s", result.ForLLM)
}
}
func TestShellTool_Background_ReturnsImmediately(t *testing.T) {
tool, err := NewExecTool("", false)
require.NoError(t, err)