@lobu/worker 3.0.5 → 3.0.6
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/USAGE.md +120 -0
- package/docs/custom-base-image.md +88 -0
- package/package.json +2 -2
- package/scripts/worker-entrypoint.sh +184 -0
- package/src/__tests__/audio-provider-suggestions.test.ts +198 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +39 -0
- package/src/__tests__/embedded-tools.test.ts +558 -0
- package/src/__tests__/instructions.test.ts +59 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/setup.ts +109 -0
- package/src/__tests__/sse-client.test.ts +48 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +70 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +125 -0
- package/src/core/url-utils.ts +9 -0
- package/src/core/workspace.ts +138 -0
- package/src/embedded/just-bash-bootstrap.ts +228 -0
- package/src/gateway/gateway-integration.ts +287 -0
- package/src/gateway/message-batcher.ts +128 -0
- package/src/gateway/sse-client.ts +955 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +146 -0
- package/src/instructions/builder.ts +80 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +290 -0
- package/src/openclaw/instructions.ts +38 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +427 -0
- package/src/openclaw/processor.ts +216 -0
- package/src/openclaw/session-context.ts +277 -0
- package/src/openclaw/tool-policy.ts +212 -0
- package/src/openclaw/tools.ts +208 -0
- package/src/openclaw/worker.ts +1792 -0
- package/src/server.ts +329 -0
- package/src/shared/audio-provider-suggestions.ts +132 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +64 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +768 -0
- package/tsconfig.json +21 -0
package/USAGE.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Worker Environment Variables
|
|
2
|
+
|
|
3
|
+
This document describes all environment variables used by Lobu Workers. Most of these are automatically set by the Gateway orchestrator and should not be manually configured.
|
|
4
|
+
|
|
5
|
+
## Gateway-Managed Variables (Auto-Set)
|
|
6
|
+
|
|
7
|
+
These variables are automatically set by the Gateway when deploying worker containers. **Do not manually configure these.**
|
|
8
|
+
|
|
9
|
+
### `DISPATCHER_URL`
|
|
10
|
+
**Description**: Gateway URL for worker-to-gateway communication
|
|
11
|
+
**Format**: HTTP URL
|
|
12
|
+
**Example**: `http://gateway:8080/worker/stream`
|
|
13
|
+
**Set by**: Gateway orchestrator
|
|
14
|
+
**Used by**: SSE connection to gateway, progress updates, session management
|
|
15
|
+
|
|
16
|
+
### `WORKER_TOKEN`
|
|
17
|
+
**Description**: Authentication token for worker-gateway communication
|
|
18
|
+
**Format**: JWT token or user-specific token
|
|
19
|
+
**Set by**: Gateway orchestrator
|
|
20
|
+
**Used by**: Worker authentication, gateway proxy authentication
|
|
21
|
+
|
|
22
|
+
### `DEPLOYMENT_NAME`
|
|
23
|
+
**Description**: Unique identifier for this worker deployment
|
|
24
|
+
**Format**: `lobu-worker-{user}-{timestamp}-{random}`
|
|
25
|
+
**Example**: `lobu-worker-u123abc-284-707819`
|
|
26
|
+
**Set by**: Gateway orchestrator
|
|
27
|
+
**Used by**: Deployment identification, logging
|
|
28
|
+
|
|
29
|
+
### `USER_ID`
|
|
30
|
+
**Description**: Platform-specific user identifier
|
|
31
|
+
**Format**: Platform-dependent (e.g., Slack user ID starts with `U`)
|
|
32
|
+
**Example**: `U0123456789`
|
|
33
|
+
**Set by**: Gateway orchestrator (updated on first message)
|
|
34
|
+
**Used by**: User-specific credential lookup, session management
|
|
35
|
+
|
|
36
|
+
### `THREAD_ID`
|
|
37
|
+
**Description**: Platform thread identifier for conversation context
|
|
38
|
+
**Format**: Platform-dependent
|
|
39
|
+
**Example**: `1234567890.123456`
|
|
40
|
+
**Set by**: Gateway orchestrator
|
|
41
|
+
**Used by**: Workspace isolation, session continuity
|
|
42
|
+
|
|
43
|
+
### `WORKSPACE_DIR`
|
|
44
|
+
**Description**: Worker workspace directory path
|
|
45
|
+
**Format**: Absolute path
|
|
46
|
+
**Default**: `/workspace`
|
|
47
|
+
**Set by**: Worker initialization
|
|
48
|
+
**Used by**: File operations, MCP process working directory
|
|
49
|
+
|
|
50
|
+
### `HOME`
|
|
51
|
+
**Description**: Home directory for worker processes
|
|
52
|
+
**Format**: Absolute path
|
|
53
|
+
**Default**: `/workspace` (to persist agent sessions)
|
|
54
|
+
**Set by**: Gateway orchestrator
|
|
55
|
+
**Used by**: Agent session storage
|
|
56
|
+
|
|
57
|
+
### `HOSTNAME`
|
|
58
|
+
**Description**: Container hostname (fallback for DEPLOYMENT_NAME)
|
|
59
|
+
**Format**: Alphanumeric string
|
|
60
|
+
**Set by**: Container runtime
|
|
61
|
+
**Used by**: Deployment identification if DEPLOYMENT_NAME not set
|
|
62
|
+
|
|
63
|
+
## MCP Configuration
|
|
64
|
+
|
|
65
|
+
### `MCP_SERVER_CONFIG`
|
|
66
|
+
**Description**: JSON configuration for MCP servers (auto-generated)
|
|
67
|
+
**Format**: JSON string
|
|
68
|
+
**Set by**: Worker initialization (from user credentials)
|
|
69
|
+
**Used by**: MCP server initialization
|
|
70
|
+
|
|
71
|
+
## Development/Debugging Variables
|
|
72
|
+
|
|
73
|
+
### `DEBUG`
|
|
74
|
+
**Description**: Enable debug logging
|
|
75
|
+
**Format**: `1` or any truthy value
|
|
76
|
+
**Example**: `DEBUG=1`
|
|
77
|
+
**Set by**: Gateway orchestrator in development mode
|
|
78
|
+
**Used by**: Enhanced logging, crash debugging
|
|
79
|
+
|
|
80
|
+
### `NODE_ENV`
|
|
81
|
+
**Description**: Environment mode
|
|
82
|
+
**Values**: `development` | `production`
|
|
83
|
+
**Set by**: Gateway orchestrator
|
|
84
|
+
**Used by**: Logging verbosity, error handling
|
|
85
|
+
|
|
86
|
+
## Container Runtime Variables
|
|
87
|
+
|
|
88
|
+
These are automatically set by the container runtime:
|
|
89
|
+
|
|
90
|
+
### Read-Only Root Filesystem
|
|
91
|
+
**Default**: Enabled (`WORKER_READONLY_ROOTFS=true` in gateway)
|
|
92
|
+
**Writable paths**:
|
|
93
|
+
- `/workspace` - Worker workspace (persistent via volume)
|
|
94
|
+
- `/tmp` - Temporary files (100MB tmpfs)
|
|
95
|
+
- `/home/bun/.cache` - Bun cache (200MB tmpfs)
|
|
96
|
+
|
|
97
|
+
### Security Configuration
|
|
98
|
+
**Capabilities**: All dropped by default (CapDrop: ALL)
|
|
99
|
+
**Privilege Escalation**: Disabled (no-new-privileges)
|
|
100
|
+
**Seccomp**: Docker's default profile
|
|
101
|
+
**AppArmor**: Docker's default profile
|
|
102
|
+
|
|
103
|
+
## Summary
|
|
104
|
+
|
|
105
|
+
**For normal operation, workers require NO manual environment configuration.** The Gateway orchestrator automatically sets all necessary variables when deploying worker containers.
|
|
106
|
+
|
|
107
|
+
### Minimal Auto-Set Variables (by Gateway):
|
|
108
|
+
```bash
|
|
109
|
+
DISPATCHER_URL=http://gateway:8080/worker/stream
|
|
110
|
+
WORKER_TOKEN=<auto-generated>
|
|
111
|
+
DEPLOYMENT_NAME=lobu-worker-<user>-<timestamp>
|
|
112
|
+
HOME=/workspace
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Optional Gateway-Passed Variables:
|
|
116
|
+
```bash
|
|
117
|
+
DEBUG=1 # Development only
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
All other configuration is handled automatically by the worker based on gateway-provided context.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Using @lobu/worker with Custom Base Images
|
|
2
|
+
|
|
3
|
+
Install and run the Lobu worker in your own Docker base image instead of extending the default `ghcr.io/lobu-ai/lobu-worker-base`.
|
|
4
|
+
|
|
5
|
+
## System Requirements
|
|
6
|
+
|
|
7
|
+
| Dependency | Version | Required |
|
|
8
|
+
|------------|---------|----------|
|
|
9
|
+
| Node.js or Bun | >= 18.0 | Yes — worker runtime |
|
|
10
|
+
| Git | >= 2.30 | Yes — code operations |
|
|
11
|
+
| Docker CLI | >= 20.10 | Optional — spawning sub-containers |
|
|
12
|
+
| Python | >= 3.9 | Optional — Python tools |
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Bun (Recommended)
|
|
17
|
+
|
|
18
|
+
```dockerfile
|
|
19
|
+
FROM oven/bun:1.2.9-alpine
|
|
20
|
+
|
|
21
|
+
RUN apk add --no-cache docker-cli git python3 py3-pip
|
|
22
|
+
|
|
23
|
+
RUN bun add -g @lobu/worker@^0.1.0
|
|
24
|
+
|
|
25
|
+
CMD ["lobu-worker"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Node.js
|
|
29
|
+
|
|
30
|
+
```dockerfile
|
|
31
|
+
FROM node:20-alpine
|
|
32
|
+
|
|
33
|
+
RUN apk add --no-cache docker-cli git python3 py3-pip curl
|
|
34
|
+
|
|
35
|
+
RUN npm install -g @lobu/worker@^0.1.0
|
|
36
|
+
|
|
37
|
+
CMD ["lobu-worker"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Company Approved Base
|
|
41
|
+
|
|
42
|
+
```dockerfile
|
|
43
|
+
FROM company-registry.example.com/ubuntu:22.04
|
|
44
|
+
|
|
45
|
+
RUN apt-get update && apt-get install -y \
|
|
46
|
+
nodejs npm docker.io git python3 curl
|
|
47
|
+
|
|
48
|
+
RUN npm install -g @lobu/worker@^0.1.0
|
|
49
|
+
|
|
50
|
+
CMD ["lobu-worker"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Environment Variables
|
|
54
|
+
|
|
55
|
+
All variables are auto-set by the gateway orchestrator. Manual configuration is not required.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Auto-set by gateway (do not configure manually)
|
|
59
|
+
DISPATCHER_URL=http://gateway:8080/worker/stream
|
|
60
|
+
WORKER_TOKEN=<auto-generated>
|
|
61
|
+
DEPLOYMENT_NAME=lobu-worker-<user>-<timestamp>
|
|
62
|
+
USER_ID=<platform-user-id>
|
|
63
|
+
HOME=/workspace
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [USAGE.md](../USAGE.md) for the full variable reference.
|
|
67
|
+
|
|
68
|
+
## Compatibility Matrix
|
|
69
|
+
|
|
70
|
+
| Base Image | Status | Notes |
|
|
71
|
+
|------------|--------|-------|
|
|
72
|
+
| `oven/bun:1.2.9` | Tested | Best performance |
|
|
73
|
+
| `node:20-alpine` | Tested | Small size |
|
|
74
|
+
| `node:20-slim` | Tested | Debian-based |
|
|
75
|
+
| `ubuntu:22.04` | Tested | Most compatible |
|
|
76
|
+
| `alpine:3.19` | Tested | Must install Node separately |
|
|
77
|
+
|
|
78
|
+
## Troubleshooting
|
|
79
|
+
|
|
80
|
+
**`lobu-worker: command not found`** — Ensure the package was installed globally (`npm install -g` or `bun add -g`).
|
|
81
|
+
|
|
82
|
+
**`git: command not found`** — Install git in your base image (`apk add git` or `apt-get install git`).
|
|
83
|
+
|
|
84
|
+
**`EACCES: permission denied`** — Run as root or add user to the docker group.
|
|
85
|
+
|
|
86
|
+
## Getting Help
|
|
87
|
+
|
|
88
|
+
- [GitHub Issues](https://github.com/lobu-ai/lobu/issues)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobu/worker",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"description": "Lobu worker runtime - run in your own Docker image or use our base image",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@hono/node-server": "^1.19.9",
|
|
38
|
-
"@lobu/core": "
|
|
38
|
+
"@lobu/core": "workspace:*",
|
|
39
39
|
"@lobu/owletto-openclaw": "^3.0.2",
|
|
40
40
|
"@mariozechner/pi-agent-core": "^0.51.6",
|
|
41
41
|
"@mariozechner/pi-ai": "^0.51.6",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Container entrypoint script for Lobu Worker
|
|
5
|
+
echo "🚀 Starting Lobu Worker..."
|
|
6
|
+
|
|
7
|
+
# Function to handle cleanup on exit
|
|
8
|
+
cleanup() {
|
|
9
|
+
echo "📦 Container shutting down, performing cleanup..."
|
|
10
|
+
|
|
11
|
+
# Kill any background processes
|
|
12
|
+
jobs -p | xargs -r kill || true
|
|
13
|
+
|
|
14
|
+
# Give processes time to exit gracefully
|
|
15
|
+
sleep 2
|
|
16
|
+
|
|
17
|
+
echo "✅ Cleanup completed"
|
|
18
|
+
exit 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Setup signal handlers for graceful shutdown
|
|
22
|
+
trap cleanup SIGTERM SIGINT
|
|
23
|
+
|
|
24
|
+
echo "🔍 Environment variables provided by orchestrator:"
|
|
25
|
+
echo " - USER_ID: ${USER_ID:-not set}"
|
|
26
|
+
echo " - CHANNEL_ID: ${CHANNEL_ID:-not set}"
|
|
27
|
+
echo " - REPOSITORY_URL: ${REPOSITORY_URL:-not set}"
|
|
28
|
+
echo " - DEPLOYMENT_NAME: ${DEPLOYMENT_NAME:-not set}"
|
|
29
|
+
|
|
30
|
+
# Basic validation for critical variables
|
|
31
|
+
if [[ -z "${USER_ID:-}" ]]; then
|
|
32
|
+
echo "❌ Error: USER_ID is required"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [[ -z "${DEPLOYMENT_NAME:-}" ]]; then
|
|
37
|
+
echo "❌ Error: DEPLOYMENT_NAME is required"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Setup workspace directory
|
|
42
|
+
echo "📁 Setting up workspace directory..."
|
|
43
|
+
WORKSPACE_DIR="/workspace"
|
|
44
|
+
|
|
45
|
+
# Workspace permissions are fixed by gateway before container starts
|
|
46
|
+
# Just verify we can write to it
|
|
47
|
+
if [ ! -w "$WORKSPACE_DIR" ]; then
|
|
48
|
+
echo "❌ Error: Cannot write to workspace directory $WORKSPACE_DIR"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Route temp files and cache to workspace-backed paths.
|
|
53
|
+
# Keep /tmp mounted for compatibility with tools that ignore TMPDIR.
|
|
54
|
+
export TMPDIR="${TMPDIR:-$WORKSPACE_DIR/.tmp}"
|
|
55
|
+
export TMP="${TMP:-$TMPDIR}"
|
|
56
|
+
export TEMP="${TEMP:-$TMPDIR}"
|
|
57
|
+
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$WORKSPACE_DIR/.cache}"
|
|
58
|
+
mkdir -p "$TMPDIR" "$XDG_CACHE_HOME"
|
|
59
|
+
|
|
60
|
+
cd "$WORKSPACE_DIR"
|
|
61
|
+
|
|
62
|
+
echo "✅ Workspace directory ready: $WORKSPACE_DIR"
|
|
63
|
+
|
|
64
|
+
# Log container information
|
|
65
|
+
echo "📊 Container Information:"
|
|
66
|
+
echo " - Session Key: $SESSION_KEY"
|
|
67
|
+
echo " - Repository: $REPOSITORY_URL"
|
|
68
|
+
echo " - Working Directory: $(pwd)"
|
|
69
|
+
echo " - Container Hostname: $(hostname)"
|
|
70
|
+
echo " - Container Memory Limit: $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo 'unknown')"
|
|
71
|
+
echo " - Container CPU Limit: $(cat /sys/fs/cgroup/cpu.max 2>/dev/null || echo 'unknown')"
|
|
72
|
+
|
|
73
|
+
# Setup git global configuration
|
|
74
|
+
echo "⚙️ Setting up git configuration..."
|
|
75
|
+
git config --global user.name "Lobu Worker"
|
|
76
|
+
git config --global user.email "lobu@noreply.github.com"
|
|
77
|
+
git config --global init.defaultBranch main
|
|
78
|
+
git config --global pull.rebase false
|
|
79
|
+
git config --global safe.directory '*'
|
|
80
|
+
|
|
81
|
+
# In development mode, ensure core package can find its dependencies
|
|
82
|
+
# The packages/ dir is mounted as a volume which may contain node_modules from host
|
|
83
|
+
if [ "${NODE_ENV}" = "development" ]; then
|
|
84
|
+
# Remove any existing node_modules that aren't symlinks (non-fatal on read-only rootfs)
|
|
85
|
+
if [ -e "/app/packages/core/node_modules" ] && [ ! -L "/app/packages/core/node_modules" ]; then
|
|
86
|
+
echo "🗑️ Removing host node_modules from /app/packages/core/"
|
|
87
|
+
rm -rf /app/packages/core/node_modules 2>/dev/null || true
|
|
88
|
+
fi
|
|
89
|
+
if [ ! -e "/app/packages/core/node_modules" ]; then
|
|
90
|
+
echo "🔗 Creating symlink for core package dependencies..."
|
|
91
|
+
ln -sf /app/node_modules /app/packages/core/node_modules 2>/dev/null || true
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Also for worker package if needed (non-fatal on read-only rootfs)
|
|
95
|
+
if [ -e "/app/packages/worker/node_modules" ] && [ ! -L "/app/packages/worker/node_modules" ]; then
|
|
96
|
+
rm -rf /app/packages/worker/node_modules 2>/dev/null || true
|
|
97
|
+
fi
|
|
98
|
+
if [ ! -e "/app/packages/worker/node_modules" ]; then
|
|
99
|
+
ln -sf /app/node_modules /app/packages/worker/node_modules 2>/dev/null || true
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Source Nix profile if installed (non-interactive shells don't source /etc/profile.d)
|
|
104
|
+
if [ -f /home/worker/.nix-profile/etc/profile.d/nix.sh ]; then
|
|
105
|
+
. /home/worker/.nix-profile/etc/profile.d/nix.sh
|
|
106
|
+
# Set NIX_PATH for nix-shell -p to find nixpkgs
|
|
107
|
+
export NIX_PATH="nixpkgs=/home/worker/.nix-defexpr/channels/nixpkgs"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Docker fallback: persist Nix store on workspace PVC via symlinks
|
|
111
|
+
# (K8s uses init container + subPath mounts instead, detected by .nix-pvc-mounted marker)
|
|
112
|
+
if [ -n "${NIX_PACKAGES:-}${NIX_FLAKE_URL:-}" ] && [ ! -d "/nix/store/.nix-pvc-mounted" ]; then
|
|
113
|
+
NIX_PVC_STORE="/workspace/.nix-store"
|
|
114
|
+
NIX_PVC_VAR="/workspace/.nix-var"
|
|
115
|
+
MARKER="/workspace/.nix-bootstrapped"
|
|
116
|
+
if [ ! -f "$MARKER" ]; then
|
|
117
|
+
echo "Bootstrapping Nix store to PVC..."
|
|
118
|
+
cp -a /nix/store "$NIX_PVC_STORE"
|
|
119
|
+
cp -a /nix/var "$NIX_PVC_VAR"
|
|
120
|
+
touch "$MARKER"
|
|
121
|
+
fi
|
|
122
|
+
rm -rf /nix/store /nix/var
|
|
123
|
+
ln -sf "$NIX_PVC_STORE" /nix/store
|
|
124
|
+
ln -sf "$NIX_PVC_VAR" /nix/var
|
|
125
|
+
echo "Nix store linked to PVC"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Nix environment activation
|
|
129
|
+
# Priority: API env vars > repo files
|
|
130
|
+
activate_nix_env() {
|
|
131
|
+
local cmd="$1"
|
|
132
|
+
|
|
133
|
+
# Check if Nix is installed
|
|
134
|
+
if ! command -v nix &> /dev/null; then
|
|
135
|
+
echo "⚠️ Nix not installed, skipping environment activation"
|
|
136
|
+
exec $cmd
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# 1. API-provided flake URL takes highest priority
|
|
140
|
+
if [ -n "${NIX_FLAKE_URL:-}" ]; then
|
|
141
|
+
echo "🔧 Activating Nix flake environment: $NIX_FLAKE_URL"
|
|
142
|
+
exec nix develop "$NIX_FLAKE_URL" --command $cmd
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# 2. API-provided packages list
|
|
146
|
+
if [ -n "${NIX_PACKAGES:-}" ]; then
|
|
147
|
+
# Convert comma-separated to space-separated
|
|
148
|
+
local packages="${NIX_PACKAGES//,/ }"
|
|
149
|
+
echo "🔧 Activating Nix packages: $packages"
|
|
150
|
+
exec nix-shell -p $packages --command "$cmd"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# 3. Check for nix files in workspace (git-based config)
|
|
154
|
+
if [ -f "$WORKSPACE_DIR/flake.nix" ]; then
|
|
155
|
+
echo "🔧 Detected flake.nix in workspace, activating..."
|
|
156
|
+
exec nix develop "$WORKSPACE_DIR" --command $cmd
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [ -f "$WORKSPACE_DIR/shell.nix" ]; then
|
|
160
|
+
echo "🔧 Detected shell.nix in workspace, activating..."
|
|
161
|
+
exec nix-shell "$WORKSPACE_DIR/shell.nix" --command "$cmd"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# 4. Check for simple .nix-packages file (one package per line)
|
|
165
|
+
if [ -f "$WORKSPACE_DIR/.nix-packages" ]; then
|
|
166
|
+
local packages=$(cat "$WORKSPACE_DIR/.nix-packages" | tr '\n' ' ')
|
|
167
|
+
echo "🔧 Detected .nix-packages file, activating: $packages"
|
|
168
|
+
exec nix-shell -p $packages --command "$cmd"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# No nix config found, run directly
|
|
172
|
+
exec $cmd
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Start the worker process
|
|
176
|
+
echo "🚀 Executing Worker..."
|
|
177
|
+
# Check if we're already in the worker directory
|
|
178
|
+
if [ "$(pwd)" != "/app/packages/worker" ]; then
|
|
179
|
+
cd /app/packages/worker || { echo "❌ Failed to cd to /app/packages/worker"; exit 1; }
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Always run from source — Bun handles TypeScript natively and this avoids
|
|
183
|
+
# CJS/ESM interop issues with ESM-only dependencies (e.g. pi-coding-agent).
|
|
184
|
+
activate_nix_env "bun run src/index.ts"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { OpenClawWorker } from "../openclaw/worker";
|
|
3
|
+
import {
|
|
4
|
+
fetchAudioProviderSuggestions,
|
|
5
|
+
normalizeAudioProviderSuggestions,
|
|
6
|
+
} from "../shared/audio-provider-suggestions";
|
|
7
|
+
import { generateAudio } from "../shared/tool-implementations";
|
|
8
|
+
|
|
9
|
+
const originalFetch = globalThis.fetch;
|
|
10
|
+
|
|
11
|
+
function extractText(result: {
|
|
12
|
+
content: Array<{ type: "text"; text: string }>;
|
|
13
|
+
}): string {
|
|
14
|
+
return result.content[0]?.text || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("audio provider suggestions", () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
globalThis.fetch = originalFetch;
|
|
20
|
+
mock.restore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("normalizes gateway capability providers into prefill IDs + display list", () => {
|
|
24
|
+
const normalized = normalizeAudioProviderSuggestions({
|
|
25
|
+
available: false,
|
|
26
|
+
providers: [
|
|
27
|
+
{ provider: "openai", name: "OpenAI" },
|
|
28
|
+
{ provider: "gemini", name: "Google Gemini" },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(normalized.available).toBe(false);
|
|
33
|
+
expect(normalized.usedFallback).toBe(false);
|
|
34
|
+
expect(normalized.providerIds).toEqual(["chatgpt", "openai", "gemini"]);
|
|
35
|
+
expect(normalized.providerDisplayList).toBe("OpenAI, Google Gemini");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("falls back safely when capability payload is malformed", () => {
|
|
39
|
+
const normalized = normalizeAudioProviderSuggestions({
|
|
40
|
+
available: true,
|
|
41
|
+
providers: [{ unexpected: "value" }],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(normalized.available).toBe(true);
|
|
45
|
+
expect(normalized.usedFallback).toBe(true);
|
|
46
|
+
expect(normalized.providerIds).toEqual(["chatgpt", "gemini", "elevenlabs"]);
|
|
47
|
+
expect(normalized.providerDisplayList).toBe("");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("falls back safely when capability fetch fails", async () => {
|
|
51
|
+
globalThis.fetch = mock(async () => {
|
|
52
|
+
throw new Error("network down");
|
|
53
|
+
}) as unknown as typeof fetch;
|
|
54
|
+
|
|
55
|
+
const normalized = await fetchAudioProviderSuggestions({
|
|
56
|
+
gatewayUrl: "http://gateway",
|
|
57
|
+
workerToken: "token",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(normalized.available).toBeNull();
|
|
61
|
+
expect(normalized.usedFallback).toBe(true);
|
|
62
|
+
expect(normalized.providerIds).toEqual(["chatgpt", "gemini", "elevenlabs"]);
|
|
63
|
+
expect(normalized.providerDisplayList).toBe("");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("GenerateAudio dynamic provider messaging", () => {
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
globalThis.fetch = originalFetch;
|
|
70
|
+
mock.restore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("uses dynamic capability providers in missing-scope guidance", async () => {
|
|
74
|
+
const fetchMock = mock(
|
|
75
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
76
|
+
const url = String(input);
|
|
77
|
+
|
|
78
|
+
if (url.endsWith("/internal/audio/capabilities")) {
|
|
79
|
+
return new Response(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
available: true,
|
|
82
|
+
providers: [
|
|
83
|
+
{ provider: "openai", name: "OpenAI" },
|
|
84
|
+
{ provider: "gemini", name: "Google Gemini" },
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (url.endsWith("/internal/audio/synthesize")) {
|
|
92
|
+
expect(init?.method).toBe("POST");
|
|
93
|
+
return new Response(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
error: "missing_scope: api.model.audio.request",
|
|
96
|
+
}),
|
|
97
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
106
|
+
|
|
107
|
+
const result = await generateAudio(
|
|
108
|
+
{
|
|
109
|
+
gatewayUrl: "http://gateway",
|
|
110
|
+
workerToken: "token",
|
|
111
|
+
channelId: "ch",
|
|
112
|
+
conversationId: "conv",
|
|
113
|
+
platform: "telegram",
|
|
114
|
+
},
|
|
115
|
+
{ text: "hello world" }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const text = extractText(result as any);
|
|
119
|
+
|
|
120
|
+
expect(text).toContain("OpenAI, Google Gemini");
|
|
121
|
+
expect(text).toContain("Ask an admin");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("OpenClawWorker audio permission hint", () => {
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
globalThis.fetch = originalFetch;
|
|
128
|
+
mock.restore();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("uses dynamic providers in admin guidance", async () => {
|
|
132
|
+
const fetchMock = mock(async (input: RequestInfo | URL) => {
|
|
133
|
+
const url = String(input);
|
|
134
|
+
if (url.endsWith("/internal/audio/capabilities")) {
|
|
135
|
+
return new Response(
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
available: true,
|
|
138
|
+
providers: [
|
|
139
|
+
{ provider: "openai", name: "OpenAI" },
|
|
140
|
+
{ provider: "elevenlabs", name: "ElevenLabs" },
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
150
|
+
|
|
151
|
+
const hint = await (
|
|
152
|
+
OpenClawWorker.prototype as any
|
|
153
|
+
).maybeBuildAudioPermissionHintMessage(
|
|
154
|
+
"Audio generation failed because token lacks api.model.audio.request",
|
|
155
|
+
"http://gateway",
|
|
156
|
+
"token"
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(hint).toContain("OpenAI, ElevenLabs");
|
|
160
|
+
expect(hint).toContain("Ask an admin");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("falls back to generic provider suggestions when capabilities lookup fails", async () => {
|
|
164
|
+
const fetchMock = mock(async () => {
|
|
165
|
+
throw new Error("capabilities unavailable");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
169
|
+
|
|
170
|
+
const hint = await (
|
|
171
|
+
OpenClawWorker.prototype as any
|
|
172
|
+
).maybeBuildAudioPermissionHintMessage(
|
|
173
|
+
"api.model.audio.request is missing",
|
|
174
|
+
"http://gateway",
|
|
175
|
+
"token"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(hint).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("OpenClawWorker auth hint messaging", () => {
|
|
183
|
+
test("routes missing provider auth to admin guidance", async () => {
|
|
184
|
+
const hint = await (
|
|
185
|
+
OpenClawWorker.prototype as any
|
|
186
|
+
).maybeBuildAuthHintMessage(
|
|
187
|
+
'Authentication failed for "openai"',
|
|
188
|
+
"openai",
|
|
189
|
+
"gpt-4.1",
|
|
190
|
+
"http://gateway",
|
|
191
|
+
"token"
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(hint).toContain("gpt-4.1");
|
|
195
|
+
expect(hint).toContain("admin");
|
|
196
|
+
expect(hint).toContain("openai");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildBinaryInvocation } from "../embedded/just-bash-bootstrap";
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of tempDirs.splice(0)) {
|
|
11
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("buildBinaryInvocation", () => {
|
|
16
|
+
test("runs node shebang scripts through node", () => {
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lobu-owletto-"));
|
|
18
|
+
tempDirs.push(dir);
|
|
19
|
+
const scriptPath = path.join(dir, "owletto");
|
|
20
|
+
fs.writeFileSync(
|
|
21
|
+
scriptPath,
|
|
22
|
+
"#!/usr/bin/env node\nconsole.log('ok');\n",
|
|
23
|
+
"utf8"
|
|
24
|
+
);
|
|
25
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
26
|
+
|
|
27
|
+
expect(buildBinaryInvocation(scriptPath, ["version"])).toEqual({
|
|
28
|
+
command: "node",
|
|
29
|
+
args: [scriptPath, "version"],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("executes normal binaries directly", () => {
|
|
34
|
+
expect(buildBinaryInvocation("/bin/echo", ["hello"])).toEqual({
|
|
35
|
+
command: "/bin/echo",
|
|
36
|
+
args: ["hello"],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|