Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Step Realtime CLI 采用分层 monorepo 架构,默认依赖方向如下:
- `extensions/realtime-aec/`:基于 Chrome 的 AEC 适配
- 其他第三方系统集成(IM / Email / storage / vector db / search backend 等)

扩展目录只负责外部系统对接,不负责核心编排语义。realtime-* 系列适配器统一依赖 `packages/realtime` 暴露的协议层。
扩展目录只负责外部系统对接,不负责核心编排语义。realtime-\* 系列适配器统一依赖 `packages/realtime` 暴露的协议层。

### `src/gateway/`

Expand Down Expand Up @@ -311,7 +311,7 @@ TUI / UI / CLI / Desktop 默认通过 `packages/sdk` 调用 gateway:
- 新增 CLI 子命令注册:`src/commands/`
- 新增本地 CLI / TUI 运行时装配:`src/runtime/`
- 新增内置工具或技能:`skills/`
- 新增第三方集成或通道(含 LLM / MCP / realtime-* 适配):`extensions/`
- 新增第三方集成或通道(含 LLM / MCP / realtime-\* 适配):`extensions/`
- 新增服务端宿主能力:`src/gateway/`
- 新增默认 CLI 客户端实现:`src/cli/`
- 新增界面与交互:`src/tui/`、`ui/`、`apps/*`
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ git clone <your-fork-url>
cd step-realtime-cli
pnpm install
pnpm step --help # run locally without building
pnpm test # run the automated test suite
pnpm check # required before opening a PR
```

Expand Down Expand Up @@ -36,6 +37,10 @@ platform-specific code (audio drivers, Chrome finder, etc.), use
`vi.skipIf` / `describe.runIf` rather than hardcoded platform skips so that
every test file still loads on every platform.

For UI, voice, or service-facing changes, also verify behavior manually with
`pnpm step` / `pnpm gateway:watch` / `pnpm tui:dev` / `pnpm ui:dev` as
appropriate for the change.

## Architecture

Read [`AGENTS.md`](./AGENTS.md) first — it lays out the layered monorepo
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

StepFun operates two independent sites; pick the one that matches where your API key was issued. The two sites do **not** share accounts or keys.

| Region | Console | API endpoint | Installer |
| --- | --- | --- | --- |
| Mainland China (default) | https://platform.stepfun.com/ | `https://api.stepfun.com` | `bash scripts/setup.sh` |
| Overseas | https://platform.stepfun.ai/ | `https://api.stepfun.ai` | `bash scripts/setup-overseas.sh` |
| Region | Console | API endpoint | Installer |
| ------------------------ | ----------------------------- | ------------------------- | -------------------------------- |
| Mainland China (default) | https://platform.stepfun.com/ | `https://api.stepfun.com` | `bash scripts/setup.sh` |
| Overseas | https://platform.stepfun.ai/ | `https://api.stepfun.ai` | `bash scripts/setup-overseas.sh` |

`scripts/setup-overseas.sh` runs the same flow as `scripts/setup.sh` and then rewrites `~/.step-cli/config.json` so both the realtime WebSocket and the models-proxy base URL point at `api.stepfun.ai`. All other flags (`--skip-build`, `--force-config`, `--uninstall`, …) are forwarded verbatim.

Expand Down
8 changes: 4 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

StepFun 提供两个相互独立的站点,请按 API Key 的发放来源选择对应安装方式。两个站点的账号与密钥**不互通**。

| 站点 | 控制台 | API 域名 | 安装脚本 |
| --- | --- | --- | --- |
| 国内(默认) | https://platform.stepfun.com/ | `https://api.stepfun.com` | `bash scripts/setup.sh` |
| 海外 | https://platform.stepfun.ai/ | `https://api.stepfun.ai` | `bash scripts/setup-overseas.sh` |
| 站点 | 控制台 | API 域名 | 安装脚本 |
| ------------ | ----------------------------- | ------------------------- | -------------------------------- |
| 国内(默认) | https://platform.stepfun.com/ | `https://api.stepfun.com` | `bash scripts/setup.sh` |
| 海外 | https://platform.stepfun.ai/ | `https://api.stepfun.ai` | `bash scripts/setup-overseas.sh` |

`scripts/setup-overseas.sh` 会先复用 `scripts/setup.sh` 的全部流程,再把 `~/.step-cli/config.json` 中实时语音的 WebSocket 端点与 models-proxy 基础地址改写为 `api.stepfun.ai`。所有其他参数(`--skip-build`、`--force-config`、`--uninstall` 等)均会原样转发。

Expand Down
4 changes: 1 addition & 3 deletions extensions/llm/src/factory.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { describe, it, expect } from "vitest";
import type {
CompletionRequest,
} from "@step-cli/protocol";
import type { CompletionRequest } from "@step-cli/protocol";
import { createChatCompletionClient } from "./factory.js";
import { AnthropicMessagesClient } from "./anthropic-client.js";
import { OpenAICompatibleClient } from "./openai-client.js";
Expand Down
83 changes: 83 additions & 0 deletions packages/core/src/policy/tool-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,89 @@ describe("ToolPolicy", () => {
expect(decision.mode).toBe("deny");
});

it("denies encoded destructive shell commands", () => {
const policy = new ToolPolicy({
mode: "confirm",
nonInteractiveApproval: "deny",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = {
command: "bash -c 'cm0gLXJmIC8= | base64 -d | sh'",
};

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("deny");
expect(decision.reason).toMatch(/dangerous command/i);
});

it("allows benign encoded text", () => {
const policy = new ToolPolicy({
mode: "confirm",
nonInteractiveApproval: "deny",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = { command: "echo SGVsbG8=" };

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("confirm");
});

it("denies destructive rm paths beyond filesystem root", () => {
const policy = new ToolPolicy({
mode: "auto",
nonInteractiveApproval: "allow",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = { command: "rm -rf /tmp/test" };

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("deny");
expect(decision.reason).toMatch(/dangerous command/i);
});

it("denies destructive find delete variants", () => {
const policy = new ToolPolicy({
mode: "auto",
nonInteractiveApproval: "allow",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = {
command: "find / -mindepth 1 -delete",
};

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("deny");
expect(decision.reason).toMatch(/dangerous command/i);
});

it("denies destructive workspace wipe variants", () => {
const policy = new ToolPolicy({
mode: "auto",
nonInteractiveApproval: "allow",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = {
command: "find . -mindepth 1 -delete",
};

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("deny");
expect(decision.reason).toMatch(/dangerous command/i);
});

it("denies git clean forced delete variants", () => {
const policy = new ToolPolicy({
mode: "auto",
nonInteractiveApproval: "allow",
});
const spec = makeToolSpec({ name: "bash", risk: "execute" });
const inspection: ToolCallInspection = { command: "git clean -fdx" };

const decision = policy.evaluate("bash", "{}", spec, inspection);
expect(decision.mode).toBe("deny");
expect(decision.reason).toMatch(/dangerous command/i);
});

// -- Per-tool override precedence --

it("per-tool override takes precedence over mode-based decision", () => {
Expand Down
39 changes: 37 additions & 2 deletions packages/core/src/policy/tool-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export interface ToolPolicyConfig {
}

const DANGEROUS_COMMAND_PATTERNS: RegExp[] = [
/\brm\s+-rf\s+\//i,
/\brm\s+-(?:[^\s-]*r[^\s-]*f|[^\s-]*f[^\s-]*r)\s+(?:--\s+)?(?:\/(?:\S*)?|~(?:\/\S*)?|\$HOME(?:\/\S*)?|\.\.?(?:\/\S*)?)(?:\s|$)/i,
/\bfind\s+(?:\/(?:\S*)?|~(?:\/\S*)?|\$HOME(?:\/\S*)?|\.\.?(?:\/\S*)?)(?:\s|$)[\s\S]*\s-delete(?:\s|$)/i,
/\bgit\s+clean\s+-[^\s]*f[^\s]*d/i,
/\bshutdown\b/i,
/\breboot\b/i,
/\bmkfs\b/i,
Expand Down Expand Up @@ -176,7 +178,21 @@ export class ToolPolicy implements ToolPermissionPolicy {

function isDangerousCommand(command: string): boolean {
const normalized = command.trim();
return DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized));
if (DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
return true;
}

return extractBase64Candidates(normalized).some((candidate) => {
try {
const decoded = Buffer.from(candidate, "base64").toString("utf8").trim();
return (
shouldInspectDecodedCommand(decoded) &&
DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(decoded))
);
} catch {
return false;
}
});
}

function shorten(text: string, maxChars: number): string {
Expand All @@ -185,3 +201,22 @@ function shorten(text: string, maxChars: number): string {
}
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
}

function extractBase64Candidates(command: string): string[] {
return command.match(/\b[A-Za-z0-9+/]{8,256}={0,2}\b/g) ?? [];
}

function shouldInspectDecodedCommand(decoded: string): boolean {
if (decoded.length < 4) {
return false;
}

if (!/^[\t\n\r\x20-\x7e]+$/.test(decoded)) {
return false;
}

return (
/[|;&`$()<>]/.test(decoded) ||
DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(decoded))
);
}
5 changes: 1 addition & 4 deletions packages/core/src/tools/security.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { describe, it, expect } from "vitest";
import {
getToolSecurityIssue,
validateToolSecurity,
} from "./security.js";
import { getToolSecurityIssue, validateToolSecurity } from "./security.js";

describe("security", () => {
// -- getToolSecurityIssue ----------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion packages/utils/src/clarification.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, it, expect } from "vitest";
import type { UserClarificationRequest, UserClarificationOption } from "@step-cli/protocol";
import type {
UserClarificationRequest,
UserClarificationOption,
} from "@step-cli/protocol";
import {
parseClarificationAnswer,
formatClarificationOption,
Expand Down
Binary file modified packages/utils/src/terminal-text.test.ts
Binary file not shown.