diff --git a/README.md b/README.md index 8f8b2493..c29eeca8 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,7 @@ Per-workstream metrics are labeled by `ws_id` (bounded by `[server].max_workstre - Redis (for message queue bridge — `pip install turnstone[mq]`) - Anthropic provider (optional — `pip install turnstone[anthropic]`) - PostgreSQL (optional, for production — `pip install turnstone[postgres]`) +- Math sandbox packages (optional — `pip install turnstone[sandbox]` for sympy, numpy, scipy, pytest) - [Git LFS](https://git-lfs.com/) (for cloning — diagram PNGs are stored in LFS) ## License diff --git a/docs/architecture.md b/docs/architecture.md index df6b3eeb..6c93cfc0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -259,7 +259,7 @@ class SessionUI(Protocol): | Class | Module | Notes | |-------|--------|-------| | `TerminalUI` | `turnstone.cli` | ANSI colors, `MarkdownRenderer`, `Spinner`, readline-based `input()` for approval | -| `WebUI` | `turnstone.server` | SSE event queue per workstream, `threading.Event` for blocking on approval/plan | +| `WebUI` | `turnstone.server` | SSE event queue per workstream + global broadcast, `threading.Event` for blocking on approval/plan. `on_state_change` sends to both per-workstream and global SSE (the browser UI uses per-workstream `state_change` events to manage busy/idle transitions; `stream_end` only finalizes markdown rendering). | | `NullUI` | `turnstone.eval` | Discards all output; `approve_tools` always returns `(True, None)` | ### WorkstreamTerminalUI diff --git a/docs/judge.md b/docs/judge.md index 15325e72..69095234 100644 --- a/docs/judge.md +++ b/docs/judge.md @@ -338,7 +338,7 @@ from the output before it enters the conversation. | Priority | Category | Risk | Examples | |----------|----------|------|----------| | 1 | Prompt injection | high | Override phrases, role injection (`{"role":"system"}`), instruction override markers | -| 2 | Credential leakage | high | API keys, private key blocks, connection strings, `.env` format secrets | +| 2 | Credential leakage | high | API keys, private key blocks, connection strings, `.env` format secrets, JSON secrets (`"api_key": "..."`, `"password": "..."`, etc.) | | 3 | Encoded payloads | medium | Script data URIs, hex shellcode sequences | | 4 | Adversarial URLs | medium | Cloud metadata endpoints, credential-bearing query parameters | | 5 | System info disclosure | low | Private IP addresses, sensitive file paths | diff --git a/docs/tools.md b/docs/tools.md index 2e262ad2..db1a1241 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -189,7 +189,8 @@ Execute a bash command and return stdout + stderr. |-----------|--------|----------|-------------| | `command` | string | yes | The bash command to execute. | -- **What it does**: Runs the command in a subprocess with a configurable timeout. Commands are sanitized and checked against a blocklist (e.g. `rm -rf /`). +- **What it does**: Runs the command in a subprocess with a configurable timeout (default 120s). Commands are sanitized and checked against a blocklist (e.g. `rm -rf /`). Environment variables containing secrets are scrubbed (`*_KEY`, `*_SECRET`, `*_TOKEN`, etc.). +- **Output format**: Stdout is returned directly. Stderr lines are prefixed with `[stderr]` so the model can distinguish them. When the command itself redirects stderr to stdout (`2>&1`), no prefix is added. Output exceeding 256KB is truncated (head + tail preserved, middle replaced with a truncation notice). - **Auto-approve**: No -- requires user confirmation. - **Agent availability**: `task_agent` only (not available to plan sub-agents). @@ -230,16 +231,20 @@ Write content to a file, creating it if needed. ### edit_file -Replace an exact string in a file with new content. +Replace exact strings in a file, or apply multiple replacements atomically. | Parameter | Type | Required | Description | |--------------|---------|----------|-------------| | `path` | string | yes | Absolute or relative file path. | -| `old_string` | string | yes | The exact text to find and replace. | -| `new_string` | string | yes | The replacement text. | +| `old_string` | string | no* | The exact text to find and replace. | +| `new_string` | string | no* | The replacement text. | | `near_line` | integer | no | Disambiguate when `old_string` matches multiple locations. | +| `edits` | array | no* | Multiple replacements to apply atomically (see below). | + +\* Provide either `old_string`+`new_string` (single edit) or `edits` array (batch), not both. - **What it does**: Finds `old_string` in the file and replaces it with `new_string`. Fails if the string is not found or matches multiple locations (unless `near_line` is provided to pick the nearest match). Requires a prior `read_file` call on the same path. +- **Batch mode**: The `edits` array accepts multiple `{old_string, new_string, near_line?}` entries applied atomically. All edits are validated before any are applied. Overlapping edits (two entries targeting the same text region) are rejected. Edits are applied in reverse file-position order so character offsets stay stable. - **Auto-approve**: No -- requires user confirmation. - **Agent availability**: `task_agent` only. @@ -270,8 +275,9 @@ Execute Python code for math and computation in a sandbox. |-----------|--------|----------|-------------| | `code` | string | yes | Python code to execute. Must use `print()` for output. | -- **What it does**: Runs Python code in a sandboxed environment with pre-imported libraries: `sympy`, `numpy`, `scipy`, `math`, `fractions`, `itertools`, `functools`, `collections`, `decimal`, `operator`, `random`, `re`, `string`. Common sympy names (`symbols`, `solve`, `simplify`, `sqrt`, `Matrix`, etc.) are pre-imported. -- **Auto-approve**: No -- requires user confirmation. +- **What it does**: Runs Python code in a sandboxed environment with pre-imported libraries: `sympy`, `numpy`, `scipy`, `math`, `fractions`, `itertools`, `functools`, `collections`, `decimal`, `operator`, `random`, `re`, `string`. Common sympy names (`symbols`, `solve`, `simplify`, `sqrt`, `Matrix`, etc.) are pre-imported. `pytest` is also available for import. +- **Installation**: `sympy`, `numpy`, `scipy`, and `pytest` require the `[sandbox]` extras group: `pip install turnstone[sandbox]` (included in `[all]`). +- **Auto-approve**: Yes. - **Agent availability**: `agent` and `task_agent`. --- diff --git a/sdk/typescript/openapi-console.json b/sdk/typescript/openapi-console.json index 45ced0f7..492f6c03 100644 --- a/sdk/typescript/openapi-console.json +++ b/sdk/typescript/openapi-console.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "turnstone Console API", - "version": "0.9.1", + "version": "0.9.2", "description": "Cluster-wide visibility and control across all turnstone nodes." }, "paths": { @@ -8583,4 +8583,4 @@ } } } -} +} \ No newline at end of file diff --git a/sdk/typescript/openapi-server.json b/sdk/typescript/openapi-server.json index 5475b3ce..19cd1379 100644 --- a/sdk/typescript/openapi-server.json +++ b/sdk/typescript/openapi-server.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "turnstone Server API", - "version": "0.9.1", + "version": "0.9.2", "description": "Single-node workstream management, chat interaction, and real-time streaming." }, "paths": { @@ -2043,4 +2043,4 @@ } } } -} +} \ No newline at end of file diff --git a/sdk/typescript/src/events.ts b/sdk/typescript/src/events.ts index fffac0f8..73472dff 100644 --- a/sdk/typescript/src/events.ts +++ b/sdk/typescript/src/events.ts @@ -38,6 +38,11 @@ export interface StreamEndEvent { type: "stream_end"; } +export interface StateChangeEvent { + type: "state_change"; + state: "idle" | "thinking" | "running" | "attention" | "error"; +} + export interface ToolInfoEvent { type: "tool_info"; items: Array>; @@ -150,6 +155,7 @@ export type ServerEvent = | ContentEvent | ReasoningEvent | StreamEndEvent + | StateChangeEvent | ToolInfoEvent | ApproveRequestEvent | ApprovalResolvedEvent @@ -247,6 +253,10 @@ export function isStreamEndEvent(e: ServerEvent): e is StreamEndEvent { return e.type === "stream_end"; } +export function isStateChangeEvent(e: ServerEvent): e is StateChangeEvent { + return e.type === "state_change"; +} + export function isToolResultEvent(e: ServerEvent): e is ToolResultEvent { return e.type === "tool_result"; } diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index a7e5c4bf..ccf510db 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -35,6 +35,7 @@ export type { ContentEvent, ReasoningEvent, StreamEndEvent, + StateChangeEvent, ToolInfoEvent, ApproveRequestEvent, ApprovalResolvedEvent, @@ -65,6 +66,7 @@ export { isReasoningEvent, isErrorEvent, isStreamEndEvent, + isStateChangeEvent, isToolResultEvent, isWsStateEvent, isApproveRequestEvent,