@chrysb/alphaclaw 0.9.17 → 0.9.19

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/README.md CHANGED
@@ -157,7 +157,35 @@ The built-in watchdog monitors gateway health and recovers from failures automat
157
157
  | `WATCHDOG_NOTIFICATIONS_DISABLED` | Optional | Disable watchdog notifications (`true`/`false`) |
158
158
  | `PORT` | Optional | Server port (default `3000`) |
159
159
  | `ALPHACLAW_ROOT_DIR` | Optional | Data directory (default `/data`) |
160
+ | `ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL` | Optional | Skip writes to `/etc/cron.d` while keeping cron config (`true`/`false`); the managed hourly script still exits when sync is disabled |
161
+ | `ALPHACLAW_GIT_SHIM_PATH` | Optional | Install the managed git auth shim at this path and prepend its directory to runtime `PATH` (default `/usr/local/bin/git`) |
162
+ | `ALPHACLAW_GIT_ASKPASS_PATH` | Optional | Install the git askpass helper at this path (default `$TMPDIR/alphaclaw-git-askpass.sh`) |
160
163
  | `TRUST_PROXY_HOPS` | Optional | Trust proxy hop count for correct client IP |
164
+ | `REMOTE_MCP_URL` | Optional | Upstream remote MCP server URL. When set together with `REMOTE_MCP_API_TOKEN`, AlphaClaw writes a managed `mcp.servers.<name>` entry to `openclaw.json` on every gateway start. |
165
+ | `REMOTE_MCP_API_TOKEN` | Optional | Bearer token for the remote MCP server. Persisted in `openclaw.json` as the `${REMOTE_MCP_API_TOKEN}` reference, never as plaintext. |
166
+ | `REMOTE_MCP_NAME` | Optional | Key under `mcp.servers.<name>`. Defaults to `remote`. Set it to label the entry (e.g. `sure`, `notion`). |
167
+ | `REMOTE_MCP_PROXY_URL` | Optional | When set, OpenClaw connects here instead of `REMOTE_MCP_URL`. Intended for a same-host scanning proxy (e.g. `pipelock mcp proxy --listen <REMOTE_MCP_PROXY_URL> --upstream <REMOTE_MCP_URL>`). Implementation is proxy-agnostic. |
168
+
169
+ ## OpenAI-compatible `/v1` proxy
170
+
171
+ AlphaClaw can expose an OpenAI-compatible API surface on the same public port as the Setup UI. It is disabled by default. Enable it from the Setup UI under General -> Features -> API; the setting is persisted in `alphaclaw.json` in the OpenClaw repo so workspace sync can commit the change.
172
+
173
+ | Path | Method | Notes |
174
+ | ------------------------------- | ------- | ------------------------------------------------------------------ |
175
+ | `/v1/chat/completions` | POST | Streams when `stream: true`. Use `model: "openclaw/default"` or `openclaw/<agentId>`. |
176
+ | `/v1/responses` | POST | OpenClaw's `/v1/responses` surface (enabled together with chat completions). |
177
+ | `/v1/embeddings` | POST | Routes to OpenClaw's embeddings endpoint. |
178
+ | `/v1/models`, `/v1/models/<id>` | GET | Lists OpenClaw agent targets. |
179
+
180
+ When enabled, the proxy forwards requests to the loopback OpenClaw gateway. AlphaClaw requires `Authorization: Bearer <OPENCLAW_GATEWAY_TOKEN>` and rejects requests when the gateway token is missing or does not match before forwarding to OpenClaw. Failed bearer-token attempts are rate-limited before proxying. The setup-UI cookie is stripped before forwarding, hop-by-hop response headers are not passed through, and `/v1` JSON request bodies are accepted up to 50 MB. When disabled or missing from `alphaclaw.json`, `/v1` requests return 404.
181
+
182
+ **Security boundary (important).** OpenClaw treats `/v1/chat/completions` as a full operator-access surface. A caller with a valid `OPENCLAW_GATEWAY_TOKEN` can run any tool the configured agent profile allows. Treat this token like an owner credential:
183
+
184
+ - Use this surface only for trusted server-to-server callers (for example, a self-hosted app that needs OpenClaw as its external assistant).
185
+ - Do not hand the gateway token to end-user clients.
186
+ - If your front door is public (Render, Fly, fly-style PaaS), make sure `SETUP_PASSWORD` is strong and that the gateway token is held by exactly one trusted backend.
187
+
188
+ When `REMOTE_MCP_URL` + `REMOTE_MCP_API_TOKEN` are set, AlphaClaw also registers an `mcp.servers.<REMOTE_MCP_NAME>` block (default key `remote`) in `openclaw.json` so the agent can call back into that remote MCP server. Set `REMOTE_MCP_PROXY_URL` to route those callbacks through a same-host scanning proxy (for example a Pipelock MCP reverse proxy running in the same container).
161
189
 
162
190
  ## Security Notes
163
191
 
package/bin/alphaclaw.js CHANGED
@@ -6,6 +6,10 @@ const os = require("os");
6
6
  const path = require("path");
7
7
  const { execSync } = require("child_process");
8
8
  const {
9
+ shouldSkipSystemCronInstall,
10
+ resolveGitAskPassPath,
11
+ resolveGitShimPath,
12
+ prependGitShimDirToPath,
9
13
  normalizeGitSyncFilePath,
10
14
  validateGitSyncFilePath,
11
15
  resolveRealGitPath,
@@ -16,6 +20,9 @@ const {
16
20
  restoreMissingOpenclawConfigFromRemote,
17
21
  } = require("../lib/cli/openclaw-config-restore");
18
22
  const { buildSecretReplacements } = require("../lib/server/helpers");
23
+ const {
24
+ migrateLegacyTelegramStreamingConfig,
25
+ } = require("../lib/server/openclaw-config-migrations");
19
26
  const {
20
27
  migrateManagedInternalFiles,
21
28
  } = require("../lib/server/internal-files-migration");
@@ -278,7 +285,7 @@ const runGitSync = () => {
278
285
  }
279
286
 
280
287
  const realGitPath = resolveRealGitPath({
281
- shimPath: "/usr/local/bin/git",
288
+ shimPath: resolveGitShimPath(),
282
289
  });
283
290
  if (!realGitPath) {
284
291
  console.error(
@@ -631,7 +638,11 @@ if (fs.existsSync(hourlyGitSyncPath)) {
631
638
  }
632
639
 
633
640
  const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
634
- if (cronEnabled) {
641
+ if (shouldSkipSystemCronInstall()) {
642
+ console.log(
643
+ "[alphaclaw] System cron setup skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
644
+ );
645
+ } else if (cronEnabled) {
635
646
  const cronContent = [
636
647
  "SHELL=/bin/bash",
637
648
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -732,7 +743,10 @@ if (fs.existsSync(configPath)) {
732
743
  if (!cfg.plugins.load) cfg.plugins.load = {};
733
744
  if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
734
745
  if (!cfg.plugins.entries) cfg.plugins.entries = {};
735
- let changed = false;
746
+ let changed = migrateLegacyTelegramStreamingConfig(cfg);
747
+ if (changed) {
748
+ console.log("[alphaclaw] Migrated legacy Telegram streaming config");
749
+ }
736
750
 
737
751
  if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
738
752
  cfg.channels.telegram = {
@@ -819,11 +833,17 @@ try {
819
833
 
820
834
  try {
821
835
  const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
822
- const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
836
+ const gitAskPassDest = resolveGitAskPassPath({
837
+ tmpDir: os.tmpdir(),
838
+ });
823
839
  const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
824
- const gitShimDest = "/usr/local/bin/git";
840
+ const gitShimDest = resolveGitShimPath();
841
+ process.env.PATH = prependGitShimDirToPath({
842
+ shimPath: gitShimDest,
843
+ });
825
844
 
826
845
  if (fs.existsSync(gitAskPassSrc)) {
846
+ fs.mkdirSync(path.dirname(gitAskPassDest), { recursive: true });
827
847
  fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
828
848
  fs.chmodSync(gitAskPassDest, 0o755);
829
849
  }
@@ -837,7 +857,9 @@ try {
837
857
  const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
838
858
  const gitShimContent = gitShimTemplate
839
859
  .replace("@@REAL_GIT@@", realGitPath)
840
- .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
860
+ .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir)
861
+ .replace("@@ASKPASS_PATH@@", gitAskPassDest);
862
+ fs.mkdirSync(path.dirname(gitShimDest), { recursive: true });
841
863
  fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
842
864
  console.log("[alphaclaw] git auth shim installed");
843
865
  }
@@ -2,6 +2,43 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
 
5
+ const isEnabledEnvFlag = (value) =>
6
+ ["1", "true", "yes", "on"].includes(
7
+ String(value || "").trim().toLowerCase(),
8
+ );
9
+
10
+ const shouldSkipSystemCronInstall = ({ env = process.env } = {}) =>
11
+ isEnabledEnvFlag(env.ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL);
12
+
13
+ const resolveGitAskPassPath = ({ env = process.env, tmpDir = "/tmp" } = {}) => {
14
+ const explicitPath = String(env.ALPHACLAW_GIT_ASKPASS_PATH || "").trim();
15
+ if (explicitPath) return explicitPath;
16
+
17
+ const runtimeTmpDir =
18
+ String(env.TMPDIR || "").trim() || String(tmpDir || "").trim() || "/tmp";
19
+ return path.join(runtimeTmpDir, "alphaclaw-git-askpass.sh");
20
+ };
21
+
22
+ const resolveGitShimPath = ({ env = process.env } = {}) => {
23
+ const explicitPath = String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim();
24
+ return explicitPath || "/usr/local/bin/git";
25
+ };
26
+
27
+ const prependGitShimDirToPath = ({
28
+ env = process.env,
29
+ shimPath = resolveGitShimPath({ env }),
30
+ } = {}) => {
31
+ if (!String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim()) {
32
+ return env.PATH || process.env.PATH || "";
33
+ }
34
+ const shimDir = path.dirname(String(shimPath || "").trim());
35
+ if (!shimDir || shimDir === ".") return env.PATH || process.env.PATH || "";
36
+ const pathEntries = String(env.PATH || process.env.PATH || "")
37
+ .split(path.delimiter)
38
+ .filter((entry) => entry && entry !== shimDir);
39
+ return [shimDir, ...pathEntries].join(path.delimiter);
40
+ };
41
+
5
42
  const normalizeGitSyncFilePath = (requestedFilePath) => {
6
43
  const rawPath = String(requestedFilePath || "").trim();
7
44
  if (!rawPath) return "";
@@ -90,6 +127,10 @@ const shouldRefreshHourlyGitSyncScript = ({
90
127
  };
91
128
 
92
129
  module.exports = {
130
+ shouldSkipSystemCronInstall,
131
+ resolveGitAskPassPath,
132
+ resolveGitShimPath,
133
+ prependGitShimDirToPath,
93
134
  normalizeGitSyncFilePath,
94
135
  validateGitSyncFilePath,
95
136
  resolveRealGitPath,
@@ -27,7 +27,7 @@ const resolveCurrentBranch = ({ execSyncImpl, openclawDir }) => {
27
27
 
28
28
  const createGitEnv = ({ fsModule, osModule, env, processId }) => {
29
29
  const githubToken = String(env.GITHUB_TOKEN || "").trim();
30
- const gitEnv = { ...env };
30
+ const gitEnv = { ...env, PATH: env.PATH || process.env.PATH };
31
31
  if (!githubToken) {
32
32
  return { gitEnv, askPassPath: "" };
33
33
  }