@matthias-hausberger/beige 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +8 -0
- package/README.md +183 -0
- package/dist/channels/registry.d.ts +14 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +14 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/telegram.d.ts +92 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +469 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/tui.d.ts +8 -0
- package/dist/channels/tui.d.ts.map +1 -0
- package/dist/channels/tui.js +574 -0
- package/dist/channels/tui.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +571 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/loader.d.ts +6 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +103 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/loader.spec.d.ts +2 -0
- package/dist/config/loader.spec.d.ts.map +1 -0
- package/dist/config/loader.spec.js +195 -0
- package/dist/config/loader.spec.js.map +1 -0
- package/dist/config/schema.d.ts +107 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +42 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/schema.spec.d.ts +2 -0
- package/dist/config/schema.spec.d.ts.map +1 -0
- package/dist/config/schema.spec.js +180 -0
- package/dist/config/schema.spec.js.map +1 -0
- package/dist/gateway/agent-manager.d.ts +138 -0
- package/dist/gateway/agent-manager.d.ts.map +1 -0
- package/dist/gateway/agent-manager.js +532 -0
- package/dist/gateway/agent-manager.js.map +1 -0
- package/dist/gateway/api.d.ts +43 -0
- package/dist/gateway/api.d.ts.map +1 -0
- package/dist/gateway/api.js +256 -0
- package/dist/gateway/api.js.map +1 -0
- package/dist/gateway/api.spec.d.ts +2 -0
- package/dist/gateway/api.spec.d.ts.map +1 -0
- package/dist/gateway/api.spec.js +256 -0
- package/dist/gateway/api.spec.js.map +1 -0
- package/dist/gateway/audit.d.ts +38 -0
- package/dist/gateway/audit.d.ts.map +1 -0
- package/dist/gateway/audit.js +82 -0
- package/dist/gateway/audit.js.map +1 -0
- package/dist/gateway/audit.spec.d.ts +2 -0
- package/dist/gateway/audit.spec.d.ts.map +1 -0
- package/dist/gateway/audit.spec.js +212 -0
- package/dist/gateway/audit.spec.js.map +1 -0
- package/dist/gateway/gateway.d.ts +27 -0
- package/dist/gateway/gateway.d.ts.map +1 -0
- package/dist/gateway/gateway.js +158 -0
- package/dist/gateway/gateway.js.map +1 -0
- package/dist/gateway/policy.d.ts +27 -0
- package/dist/gateway/policy.d.ts.map +1 -0
- package/dist/gateway/policy.js +40 -0
- package/dist/gateway/policy.js.map +1 -0
- package/dist/gateway/policy.spec.d.ts +2 -0
- package/dist/gateway/policy.spec.d.ts.map +1 -0
- package/dist/gateway/policy.spec.js +121 -0
- package/dist/gateway/policy.spec.js.map +1 -0
- package/dist/gateway/provider-health.d.ts +83 -0
- package/dist/gateway/provider-health.d.ts.map +1 -0
- package/dist/gateway/provider-health.js +219 -0
- package/dist/gateway/provider-health.js.map +1 -0
- package/dist/gateway/provider-health.spec.d.ts +2 -0
- package/dist/gateway/provider-health.spec.d.ts.map +1 -0
- package/dist/gateway/provider-health.spec.js +278 -0
- package/dist/gateway/provider-health.spec.js.map +1 -0
- package/dist/gateway/session-settings.d.ts +62 -0
- package/dist/gateway/session-settings.d.ts.map +1 -0
- package/dist/gateway/session-settings.js +91 -0
- package/dist/gateway/session-settings.js.map +1 -0
- package/dist/gateway/session-settings.spec.d.ts +2 -0
- package/dist/gateway/session-settings.spec.d.ts.map +1 -0
- package/dist/gateway/session-settings.spec.js +141 -0
- package/dist/gateway/session-settings.spec.js.map +1 -0
- package/dist/gateway/sessions.d.ts +68 -0
- package/dist/gateway/sessions.d.ts.map +1 -0
- package/dist/gateway/sessions.js +177 -0
- package/dist/gateway/sessions.js.map +1 -0
- package/dist/gateway/sessions.spec.d.ts +2 -0
- package/dist/gateway/sessions.spec.d.ts.map +1 -0
- package/dist/gateway/sessions.spec.js +190 -0
- package/dist/gateway/sessions.spec.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +39 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +144 -0
- package/dist/install.js.map +1 -0
- package/dist/sandbox/manager.d.ts +63 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +294 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/registry.d.ts +11 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/registry.js +86 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/skills/registry.spec.d.ts +2 -0
- package/dist/skills/registry.spec.d.ts.map +1 -0
- package/dist/skills/registry.spec.js +220 -0
- package/dist/skills/registry.spec.js.map +1 -0
- package/dist/socket/protocol.d.ts +21 -0
- package/dist/socket/protocol.d.ts.map +1 -0
- package/dist/socket/protocol.js +7 -0
- package/dist/socket/protocol.js.map +1 -0
- package/dist/socket/protocol.spec.d.ts +2 -0
- package/dist/socket/protocol.spec.d.ts.map +1 -0
- package/dist/socket/protocol.spec.js +135 -0
- package/dist/socket/protocol.spec.js.map +1 -0
- package/dist/socket/server.d.ts +21 -0
- package/dist/socket/server.d.ts.map +1 -0
- package/dist/socket/server.js +133 -0
- package/dist/socket/server.js.map +1 -0
- package/dist/socket/server.spec.d.ts +2 -0
- package/dist/socket/server.spec.d.ts.map +1 -0
- package/dist/socket/server.spec.js +333 -0
- package/dist/socket/server.spec.js.map +1 -0
- package/dist/test/fixtures.d.ts +47 -0
- package/dist/test/fixtures.d.ts.map +1 -0
- package/dist/test/fixtures.js +144 -0
- package/dist/test/fixtures.js.map +1 -0
- package/dist/toolkit/index.d.ts +4 -0
- package/dist/toolkit/index.d.ts.map +1 -0
- package/dist/toolkit/index.js +4 -0
- package/dist/toolkit/index.js.map +1 -0
- package/dist/toolkit/installer.d.ts +26 -0
- package/dist/toolkit/installer.d.ts.map +1 -0
- package/dist/toolkit/installer.js +247 -0
- package/dist/toolkit/installer.js.map +1 -0
- package/dist/toolkit/registry.d.ts +19 -0
- package/dist/toolkit/registry.d.ts.map +1 -0
- package/dist/toolkit/registry.js +119 -0
- package/dist/toolkit/registry.js.map +1 -0
- package/dist/toolkit/registry.spec.d.ts +2 -0
- package/dist/toolkit/registry.spec.d.ts.map +1 -0
- package/dist/toolkit/registry.spec.js +194 -0
- package/dist/toolkit/registry.spec.js.map +1 -0
- package/dist/toolkit/schema.d.ts +61 -0
- package/dist/toolkit/schema.d.ts.map +1 -0
- package/dist/toolkit/schema.js +116 -0
- package/dist/toolkit/schema.js.map +1 -0
- package/dist/toolkit/schema.spec.d.ts +2 -0
- package/dist/toolkit/schema.spec.d.ts.map +1 -0
- package/dist/toolkit/schema.spec.js +202 -0
- package/dist/toolkit/schema.spec.js.map +1 -0
- package/dist/tools/core.d.ts +10 -0
- package/dist/tools/core.d.ts.map +1 -0
- package/dist/tools/core.js +246 -0
- package/dist/tools/core.js.map +1 -0
- package/dist/tools/core.spec.d.ts +2 -0
- package/dist/tools/core.spec.d.ts.map +1 -0
- package/dist/tools/core.spec.js +315 -0
- package/dist/tools/core.spec.js.map +1 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +62 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/registry.spec.d.ts +2 -0
- package/dist/tools/registry.spec.d.ts.map +1 -0
- package/dist/tools/registry.spec.js +228 -0
- package/dist/tools/registry.spec.js.map +1 -0
- package/dist/tools/runner.d.ts +25 -0
- package/dist/tools/runner.d.ts.map +1 -0
- package/dist/tools/runner.js +35 -0
- package/dist/tools/runner.js.map +1 -0
- package/dist/tools/runner.spec.d.ts +2 -0
- package/dist/tools/runner.spec.d.ts.map +1 -0
- package/dist/tools/runner.spec.js +129 -0
- package/dist/tools/runner.spec.js.map +1 -0
- package/dist/types/session.d.ts +8 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +23 -0
- package/dist/types/session.js.map +1 -0
- package/package.json +76 -0
- package/tools/README.md +1 -0
- package/tools/kv/README.md +150 -0
- package/tools/kv/index.ts +149 -0
- package/tools/kv/tool.json +23 -0
- package/tools/message/README.md +53 -0
- package/tools/message/index.ts +183 -0
- package/tools/message/tool.json +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matthias-hausberger/beige",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Secure sandboxed agent system",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"beige": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"tools"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"beige": "tsx src/cli.ts",
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"start": "node dist/cli.js",
|
|
28
|
+
"build:sandbox": "docker build -t beige-sandbox:latest ./sandbox",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"test:coverage": "vitest run --coverage",
|
|
32
|
+
"docs:dev": "cd docs && mintlify dev --port 3333",
|
|
33
|
+
"docs:build": "cd docs && mintlify build",
|
|
34
|
+
"docs:deploy": "cd docs && mintlify deploy"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mariozechner/pi-ai": "^0.56.1",
|
|
38
|
+
"@mariozechner/pi-coding-agent": "latest",
|
|
39
|
+
"@sinclair/typebox": "^0.34.0",
|
|
40
|
+
"dockerode": "^4.0.5",
|
|
41
|
+
"grammy": "^1.35.0",
|
|
42
|
+
"json5": "^2.2.3",
|
|
43
|
+
"tar": "^7.5.11"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/dockerode": "^3.3.38",
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"@types/tar": "^7.0.87",
|
|
49
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
50
|
+
"mintlify": "^4.2.417",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"vitest": "^4.0.18"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.0.0"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"ai",
|
|
60
|
+
"agent",
|
|
61
|
+
"sandbox",
|
|
62
|
+
"docker",
|
|
63
|
+
"llm",
|
|
64
|
+
"claude",
|
|
65
|
+
"anthropic"
|
|
66
|
+
],
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/matthias-hausberger/beige.git"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/matthias-hausberger/beige/issues"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://github.com/matthias-hausberger/beige#readme",
|
|
75
|
+
"license": "MIT"
|
|
76
|
+
}
|
package/tools/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder showcases examples for tools. They are NOT added to an agent by default and NOT used.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# KV Tool
|
|
2
|
+
|
|
3
|
+
Simple key-value store. Data persists on the gateway host at `~/.beige/data/kv.json` across sessions and gateway restarts.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
/tools/bin/kv set <key> <value> # Store a value
|
|
9
|
+
/tools/bin/kv get <key> # Retrieve a value
|
|
10
|
+
/tools/bin/kv del <key> # Delete a key
|
|
11
|
+
/tools/bin/kv list # List all keys with their values
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
### Basic Operations
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
# Store a travel note
|
|
20
|
+
/tools/bin/kv set trip:paris "Flying March 15, Hotel Lumiere"
|
|
21
|
+
|
|
22
|
+
# Retrieve it
|
|
23
|
+
/tools/bin/kv get trip:paris
|
|
24
|
+
# → Flying March 15, Hotel Lumiere
|
|
25
|
+
|
|
26
|
+
# List all stored keys
|
|
27
|
+
/tools/bin/kv list
|
|
28
|
+
# → trip:paris = Flying March 15, Hotel Lumiere
|
|
29
|
+
|
|
30
|
+
# Delete
|
|
31
|
+
/tools/bin/kv del trip:paris
|
|
32
|
+
# → Deleted: trip:paris
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Using Keys with Namespaces
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
# Organize with colon-separated namespaces
|
|
39
|
+
/tools/bin/kv set user:alice:email "alice@example.com"
|
|
40
|
+
/tools/bin/kv set user:bob:email "bob@example.com"
|
|
41
|
+
/tools/bin/kv set config:timezone "Europe/Berlin"
|
|
42
|
+
|
|
43
|
+
# List shows all keys
|
|
44
|
+
/tools/bin/kv list
|
|
45
|
+
# → user:alice:email = alice@example.com
|
|
46
|
+
# → user:bob:email = bob@example.com
|
|
47
|
+
# → config:timezone = Europe/Berlin
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Storing JSON Data
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# Store structured data as JSON string
|
|
54
|
+
/tools/bin/kv set project:config '{"name": "beige", "version": "0.1.0"}'
|
|
55
|
+
|
|
56
|
+
# Retrieve and parse with jq
|
|
57
|
+
/tools/bin/kv get project:config | jq -r '.name'
|
|
58
|
+
# → beige
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Error Handling
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
# Getting a non-existent key
|
|
65
|
+
/tools/bin/kv get nonexistent
|
|
66
|
+
# → Key not found: nonexistent
|
|
67
|
+
# (exit code: 1)
|
|
68
|
+
|
|
69
|
+
# Deleting a non-existent key
|
|
70
|
+
/tools/bin/kv del nonexistent
|
|
71
|
+
# → Key not found: nonexistent
|
|
72
|
+
# (exit code: 1)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Access Control
|
|
76
|
+
|
|
77
|
+
The commands available to an agent are controlled via the tool's `config` block in `config.json5`.
|
|
78
|
+
Two optional fields let you allowlist and/or denylist specific commands:
|
|
79
|
+
|
|
80
|
+
| Config field | Type | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `allowCommands` | `string \| string[]` | all commands | Only these commands are permitted. |
|
|
83
|
+
| `denyCommands` | `string \| string[]` | *(none)* | These commands are always blocked. Deny beats allow. |
|
|
84
|
+
|
|
85
|
+
**Example — read-only agent** (can only `get` and `list`):
|
|
86
|
+
|
|
87
|
+
```json5
|
|
88
|
+
tools: {
|
|
89
|
+
"kv-readonly": {
|
|
90
|
+
path: "./tools/kv",
|
|
91
|
+
target: "gateway",
|
|
92
|
+
config: {
|
|
93
|
+
allowCommands: ["get", "list"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
agents: {
|
|
98
|
+
reader: { tools: ["kv-readonly"] },
|
|
99
|
+
},
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Example — write-only agent** (can `set` and `del`, cannot read):
|
|
103
|
+
|
|
104
|
+
```json5
|
|
105
|
+
tools: {
|
|
106
|
+
"kv-writeonly": {
|
|
107
|
+
path: "./tools/kv",
|
|
108
|
+
target: "gateway",
|
|
109
|
+
config: {
|
|
110
|
+
allowCommands: ["set", "del"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Example — deny a single command on an otherwise full-access tool** (no `del`):
|
|
117
|
+
|
|
118
|
+
```json5
|
|
119
|
+
tools: {
|
|
120
|
+
"kv-nodelete": {
|
|
121
|
+
path: "./tools/kv",
|
|
122
|
+
target: "gateway",
|
|
123
|
+
config: {
|
|
124
|
+
denyCommands: ["del"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When a denied command is called, the tool exits with code `1` and prints a clear error:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Permission denied: command 'del' is not allowed for this agent.
|
|
134
|
+
Permitted commands: set, get, list
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
138
|
+
|
|
139
|
+
- Keys and values are strings.
|
|
140
|
+
- Values with spaces must be passed as a single argument (the tool joins all args after the key).
|
|
141
|
+
- Data is stored as JSON on the gateway host at `~/.beige/data/kv.json`. Agents cannot access the raw storage file directly — they must use the tool commands.
|
|
142
|
+
- The same physical KV store is shared across all agents. Use `allowCommands` / `denyCommands` to restrict which operations each agent can perform, not which keys it can see.
|
|
143
|
+
- If you need key-level isolation between agents, create multiple tool instances with different configs (future enhancement).
|
|
144
|
+
|
|
145
|
+
## Implementation Details
|
|
146
|
+
|
|
147
|
+
- **Target**: Gateway (runs on the host, not in the sandbox)
|
|
148
|
+
- **Storage**: `~/.beige/data/kv.json`
|
|
149
|
+
- **Protocol**: Tool launcher calls back to gateway via Unix socket
|
|
150
|
+
- **Type**: See `tools/kv/index.ts` for the handler implementation
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
// ToolHandler type is defined here inline so this file is self-contained.
|
|
6
|
+
// It can be installed anywhere (e.g. ~/.beige/tools/kv/) without needing the
|
|
7
|
+
// beige source tree.
|
|
8
|
+
type ToolHandler = (args: string[], config?: Record<string, unknown>) => Promise<{ output: string; exitCode: number }>;
|
|
9
|
+
|
|
10
|
+
/** All commands the KV tool exposes. */
|
|
11
|
+
const ALL_COMMANDS = ["set", "get", "del", "list"] as const;
|
|
12
|
+
type KVCommand = (typeof ALL_COMMANDS)[number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve which commands are permitted given the tool config.
|
|
16
|
+
*
|
|
17
|
+
* Config fields (both optional, strings or arrays of strings):
|
|
18
|
+
* allowCommands — whitelist; only these commands are permitted.
|
|
19
|
+
* Defaults to all commands when absent.
|
|
20
|
+
* denyCommands — blacklist; these commands are always blocked,
|
|
21
|
+
* even if present in allowCommands.
|
|
22
|
+
*
|
|
23
|
+
* Precedence: deny beats allow.
|
|
24
|
+
*/
|
|
25
|
+
function resolveAllowedCommands(config: Record<string, unknown>): Set<KVCommand> {
|
|
26
|
+
const toArray = (value: unknown): string[] => {
|
|
27
|
+
if (Array.isArray(value)) return value.map(String);
|
|
28
|
+
if (typeof value === "string") return [value];
|
|
29
|
+
return [];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const allowed = new Set<KVCommand>(
|
|
33
|
+
config.allowCommands !== undefined
|
|
34
|
+
? (toArray(config.allowCommands).filter((c) =>
|
|
35
|
+
(ALL_COMMANDS as readonly string[]).includes(c)
|
|
36
|
+
) as KVCommand[])
|
|
37
|
+
: ALL_COMMANDS
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
for (const cmd of toArray(config.denyCommands)) {
|
|
41
|
+
allowed.delete(cmd as KVCommand);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return allowed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* KV Tool — Simple key-value store that persists to disk.
|
|
49
|
+
* Executes on the gateway host.
|
|
50
|
+
*
|
|
51
|
+
* Commands:
|
|
52
|
+
* set <key> <value> — Store a value
|
|
53
|
+
* get <key> — Retrieve a value
|
|
54
|
+
* del <key> — Delete a key
|
|
55
|
+
* list — List all keys
|
|
56
|
+
*
|
|
57
|
+
* Config:
|
|
58
|
+
* allowCommands — only permit these commands (default: all)
|
|
59
|
+
* denyCommands — always block these commands (deny beats allow)
|
|
60
|
+
*/
|
|
61
|
+
export function createHandler(config: Record<string, unknown>): ToolHandler {
|
|
62
|
+
const storePath = resolve(homedir(), ".beige", "data", "kv.json");
|
|
63
|
+
mkdirSync(resolve(homedir(), ".beige", "data"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
const allowedCommands = resolveAllowedCommands(config);
|
|
66
|
+
|
|
67
|
+
function loadStore(): Record<string, string> {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(readFileSync(storePath, "utf-8"));
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function saveStore(store: Record<string, string>): void {
|
|
76
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return async (args: string[]) => {
|
|
80
|
+
const command = args[0];
|
|
81
|
+
|
|
82
|
+
// Access-control check — runs before any business logic.
|
|
83
|
+
if (command && !allowedCommands.has(command as KVCommand)) {
|
|
84
|
+
const permitted = [...allowedCommands].join(", ") || "(none)";
|
|
85
|
+
return {
|
|
86
|
+
output: `Permission denied: command '${command}' is not allowed for this agent.\nPermitted commands: ${permitted}`,
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
switch (command) {
|
|
92
|
+
case "set": {
|
|
93
|
+
const key = args[1];
|
|
94
|
+
const value = args.slice(2).join(" ");
|
|
95
|
+
if (!key || !value) {
|
|
96
|
+
return { output: "Usage: kv set <key> <value>", exitCode: 1 };
|
|
97
|
+
}
|
|
98
|
+
const store = loadStore();
|
|
99
|
+
store[key] = value;
|
|
100
|
+
saveStore(store);
|
|
101
|
+
return { output: `OK: ${key} = ${value}`, exitCode: 0 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "get": {
|
|
105
|
+
const key = args[1];
|
|
106
|
+
if (!key) {
|
|
107
|
+
return { output: "Usage: kv get <key>", exitCode: 1 };
|
|
108
|
+
}
|
|
109
|
+
const store = loadStore();
|
|
110
|
+
if (key in store) {
|
|
111
|
+
return { output: store[key], exitCode: 0 };
|
|
112
|
+
}
|
|
113
|
+
return { output: `Key not found: ${key}`, exitCode: 1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "del": {
|
|
117
|
+
const key = args[1];
|
|
118
|
+
if (!key) {
|
|
119
|
+
return { output: "Usage: kv del <key>", exitCode: 1 };
|
|
120
|
+
}
|
|
121
|
+
const store = loadStore();
|
|
122
|
+
if (key in store) {
|
|
123
|
+
delete store[key];
|
|
124
|
+
saveStore(store);
|
|
125
|
+
return { output: `Deleted: ${key}`, exitCode: 0 };
|
|
126
|
+
}
|
|
127
|
+
return { output: `Key not found: ${key}`, exitCode: 1 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "list": {
|
|
131
|
+
const store = loadStore();
|
|
132
|
+
const keys = Object.keys(store);
|
|
133
|
+
if (keys.length === 0) {
|
|
134
|
+
return { output: "(empty)", exitCode: 0 };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
output: keys.map((k) => `${k} = ${store[k]}`).join("\n"),
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
return {
|
|
144
|
+
output: `Unknown command: ${command}\nUsage: kv <set|get|del|list> [args...]`,
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kv",
|
|
3
|
+
"description": "Simple key-value store. Store and retrieve values by key. Data persists across sessions.",
|
|
4
|
+
"commands": [
|
|
5
|
+
"set <key> <value> — Store a value",
|
|
6
|
+
"get <key> — Retrieve a value",
|
|
7
|
+
"del <key> — Delete a key",
|
|
8
|
+
"list — List all keys"
|
|
9
|
+
],
|
|
10
|
+
"target": "gateway",
|
|
11
|
+
"config": {
|
|
12
|
+
"allowCommands": {
|
|
13
|
+
"type": "string | string[]",
|
|
14
|
+
"description": "Whitelist of permitted commands. Defaults to all commands when absent.",
|
|
15
|
+
"example": ["get", "list"]
|
|
16
|
+
},
|
|
17
|
+
"denyCommands": {
|
|
18
|
+
"type": "string | string[]",
|
|
19
|
+
"description": "Blacklist of blocked commands. Takes precedence over allowCommands.",
|
|
20
|
+
"example": ["del"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Message Tool
|
|
2
|
+
|
|
3
|
+
Send messages to users through configured channels (currently Telegram only).
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### Send to Current Session
|
|
8
|
+
|
|
9
|
+
Send a message to the current session's channel/chat/thread:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
/tools/bin/message --to-current-session -- Hello! This is a reply.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This only works when called from within an active LLM session. If called from a standalone script, you'll get an error and should use explicit targeting instead.
|
|
16
|
+
|
|
17
|
+
### Send to Specific Telegram Chat
|
|
18
|
+
|
|
19
|
+
Send to a specific chat (proactive messaging):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
/tools/bin/message telegram 123456789 -- Hello! This is a proactive message.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Send to a specific thread in a Telegram forum:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
/tools/bin/message telegram 123456789 42 -- Hello from the thread!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### With Formatting
|
|
32
|
+
|
|
33
|
+
Use `--parse-mode` for formatted messages (Markdown or HTML):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Markdown
|
|
37
|
+
/tools/bin/message telegram 123456789 -- --parse-mode markdown -- **Bold** and _italic_ text
|
|
38
|
+
|
|
39
|
+
# HTML
|
|
40
|
+
/tools/bin/message telegram 123456789 -- --parse-mode html -- <b>Bold</b> and <i>italic</i> text
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note: Telegram uses MarkdownV2 syntax. See [Telegram's formatting docs](https://core.telegram.org/bots/api#markdownv2-style) for details.
|
|
44
|
+
|
|
45
|
+
## Error Handling
|
|
46
|
+
|
|
47
|
+
- **No session context**: When using `--to-current-session` from a script, you'll get an error with guidance to use explicit targeting.
|
|
48
|
+
- **Unsupported channel**: If the current session's channel doesn't support messaging (e.g., TUI), you'll get an error.
|
|
49
|
+
- **Channel not available**: If Telegram isn't enabled in the gateway config, explicit Telegram commands will fail.
|
|
50
|
+
|
|
51
|
+
## Long Messages
|
|
52
|
+
|
|
53
|
+
Messages longer than Telegram's 4096 character limit are automatically split into multiple messages.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
type ToolHandler = (
|
|
2
|
+
args: string[],
|
|
3
|
+
config?: Record<string, unknown>,
|
|
4
|
+
sessionContext?: SessionContext
|
|
5
|
+
) => Promise<{ output: string; exitCode: number }>;
|
|
6
|
+
|
|
7
|
+
interface SessionContext {
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
channel: string;
|
|
10
|
+
chatId?: string;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ToolHandlerContext {
|
|
15
|
+
channelRegistry?: ChannelRegistry;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChannelRegistry {
|
|
19
|
+
get(channel: string): ChannelAdapter | undefined;
|
|
20
|
+
has(channel: string): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChannelAdapter {
|
|
24
|
+
sendMessage(
|
|
25
|
+
chatId: string,
|
|
26
|
+
threadId: string | undefined,
|
|
27
|
+
text: string,
|
|
28
|
+
options?: { parseMode?: "html" | "markdown" }
|
|
29
|
+
): Promise<void>;
|
|
30
|
+
supportsMessaging(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ParsedCommand {
|
|
34
|
+
action: "current" | "telegram";
|
|
35
|
+
chatId?: string;
|
|
36
|
+
threadId?: string;
|
|
37
|
+
parseMode?: "html" | "markdown";
|
|
38
|
+
text: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(args: string[]): ParsedCommand | { error: string } {
|
|
42
|
+
if (args.length === 0) {
|
|
43
|
+
return { error: "No arguments provided. Usage: message --to-current-session -- <text> | message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args[0] === "--to-current-session") {
|
|
47
|
+
const doubleDashIndex = args.indexOf("--");
|
|
48
|
+
if (doubleDashIndex === -1 || doubleDashIndex === args.length - 1) {
|
|
49
|
+
return { error: "Missing message text after --. Usage: message --to-current-session -- <text>" };
|
|
50
|
+
}
|
|
51
|
+
const textParts: string[] = [];
|
|
52
|
+
let i = doubleDashIndex + 1;
|
|
53
|
+
while (i < args.length) {
|
|
54
|
+
textParts.push(args[i]);
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
action: "current",
|
|
59
|
+
text: textParts.join(" "),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (args[0] === "telegram") {
|
|
64
|
+
if (args.length < 3) {
|
|
65
|
+
return { error: "Missing arguments. Usage: message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const chatId = args[1];
|
|
69
|
+
let threadId: string | undefined;
|
|
70
|
+
let parseMode: "html" | "markdown" | undefined;
|
|
71
|
+
let textStartIndex = 2;
|
|
72
|
+
|
|
73
|
+
// Look for threadId (must be before the final --)
|
|
74
|
+
const lastDoubleDash = args.lastIndexOf("--");
|
|
75
|
+
if (lastDoubleDash === -1 || lastDoubleDash === args.length - 1) {
|
|
76
|
+
return { error: "Missing message text after --. Usage: message telegram <chatId> [-- <threadId>] -- [--parse-mode <mode>] -- <text>" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for --parse-mode before the final --
|
|
80
|
+
for (let i = 2; i < lastDoubleDash; i++) {
|
|
81
|
+
if (args[i] === "--parse-mode" && i + 1 < lastDoubleDash) {
|
|
82
|
+
const mode = args[i + 1];
|
|
83
|
+
if (mode === "html" || mode === "markdown") {
|
|
84
|
+
parseMode = mode;
|
|
85
|
+
} else {
|
|
86
|
+
return { error: `Invalid parse mode: ${mode}. Must be 'html' or 'markdown'.` };
|
|
87
|
+
}
|
|
88
|
+
} else if (!isNaN(Number(args[i])) && !threadId && args[i - 1] !== "--parse-mode") {
|
|
89
|
+
// If it's a number and we haven't set threadId yet, it might be threadId
|
|
90
|
+
threadId = args[i];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const text = args.slice(lastDoubleDash + 1).join(" ");
|
|
95
|
+
if (!text) {
|
|
96
|
+
return { error: "Missing message text after --" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
action: "telegram",
|
|
101
|
+
chatId,
|
|
102
|
+
threadId,
|
|
103
|
+
parseMode,
|
|
104
|
+
text,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { error: `Unknown action: ${args[0]}. Use '--to-current-session' or 'telegram'.` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createHandler(config: Record<string, unknown>, context: ToolHandlerContext): ToolHandler {
|
|
112
|
+
const channelRegistry = context.channelRegistry;
|
|
113
|
+
|
|
114
|
+
return async (args: string[], _config?: Record<string, unknown>, sessionContext?: SessionContext) => {
|
|
115
|
+
const parsed = parseArgs(args);
|
|
116
|
+
if ("error" in parsed) {
|
|
117
|
+
return { output: `Error: ${parsed.error}`, exitCode: 1 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (parsed.action === "current") {
|
|
121
|
+
if (!sessionContext) {
|
|
122
|
+
return {
|
|
123
|
+
output: "Error: No session context available. This command must be run from within an active LLM session, not from a standalone script.\n\nUse explicit targeting instead: message telegram <chatId> -- <text>",
|
|
124
|
+
exitCode: 1,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const adapter = channelRegistry?.get(sessionContext.channel);
|
|
129
|
+
if (!adapter) {
|
|
130
|
+
return {
|
|
131
|
+
output: `Error: Channel '${sessionContext.channel}' is not available or not registered.`,
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!adapter.supportsMessaging()) {
|
|
137
|
+
return {
|
|
138
|
+
output: `Error: The current session's channel ('${sessionContext.channel}') doesn't support messaging.`,
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!sessionContext.chatId) {
|
|
144
|
+
return {
|
|
145
|
+
output: `Error: The current session does not have a chat ID. Cannot send message.`,
|
|
146
|
+
exitCode: 1,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await adapter.sendMessage(sessionContext.chatId, sessionContext.threadId, parsed.text);
|
|
152
|
+
return { output: "Message sent successfully.", exitCode: 0 };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return { output: `Error sending message: ${msg}`, exitCode: 1 };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (parsed.action === "telegram") {
|
|
160
|
+
if (!parsed.chatId) {
|
|
161
|
+
return { output: "Error: Missing chat ID for Telegram message.", exitCode: 1 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const adapter = channelRegistry?.get("telegram");
|
|
165
|
+
if (!adapter) {
|
|
166
|
+
return {
|
|
167
|
+
output: "Error: Telegram channel is not available. Make sure Telegram is enabled in the gateway config.",
|
|
168
|
+
exitCode: 1,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await adapter.sendMessage(parsed.chatId, parsed.threadId, parsed.text, { parseMode: parsed.parseMode });
|
|
174
|
+
return { output: "Message sent successfully to Telegram.", exitCode: 0 };
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
return { output: `Error sending message to Telegram: ${msg}`, exitCode: 1 };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { output: "Error: Unknown action.", exitCode: 1 };
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "message",
|
|
3
|
+
"description": "Send messages to users through configured channels (Telegram). Can reply to the current session or send proactively to any chat.",
|
|
4
|
+
"commands": [
|
|
5
|
+
"--to-current-session -- <text> — Send to the current session's channel/chat/thread",
|
|
6
|
+
"telegram <chatId> -- <text> — Send to a specific Telegram chat",
|
|
7
|
+
"telegram <chatId> <threadId> -- <text> — Send to a specific Telegram thread",
|
|
8
|
+
"telegram <chatId> -- --parse-mode <mode> -- <text> — Send with formatting (html|markdown)"
|
|
9
|
+
],
|
|
10
|
+
"target": "gateway"
|
|
11
|
+
}
|