@clawscarf/cli 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
@@ -32,4 +32,4 @@ frontend native_node
32
32
  default_backend native_gateway
33
33
 
34
34
  backend native_gateway
35
- server gateway "host.docker.internal:${CLAWSCARF_NODE_GATEWAY_PORT}" resolvers docker init-addr libc,none
35
+ server gateway "application:${CLAWSCARF_NODE_GATEWAY_PORT}" resolvers docker init-addr libc,none
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-alpha.1",
2
+ "version": "0.1.0-alpha.2",
3
3
  "engines": {
4
4
  "node": ">=24.16.0 <25 || >=26.1.0"
5
5
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "team-server",
3
3
  "version": "0.1.0",
4
- "runtime": "../../runtime/releases/0.1.0-alpha.1.json",
4
+ "runtime": "../../runtime/releases/0.1.0-alpha.2.json",
5
5
  "name": "Team server",
6
6
  "description": "A protected OpenClaw server for your team, with hosted login, managed models and optional Connections.",
7
7
  "maturity": "supported",
package/release/README.md CHANGED
@@ -14,9 +14,10 @@ Recipes and packs ship with the CLI; there is no separate registry or dependency
14
14
  The [model catalog](../deploy/models/catalog.json) is also a CLI asset. Recipe IDs select
15
15
  catalog entries; model protocols and limits have one definition.
16
16
 
17
- ## Current development use
17
+ ## Published and development use
18
18
 
19
19
  ```sh
20
+ npm install -g @clawscarf/cli@next
20
21
  clawscarf configure
21
22
  # Or use staging for hosted login and Connections:
22
23
  clawscarf configure --cloud-url https://cloud-staging.clawscarf.com
@@ -33,7 +34,13 @@ locally built Docker image IDs and checksum-pinned OpenShell tools under ignored
33
34
  published, clean-machine release**. Definitions belong in the tracked directories
34
35
  above. Compiled images, binaries and test artifacts are not source definitions.
35
36
 
36
- The current platform is macOS arm64 with Docker Desktop. Setup validates the host,
37
+ Published candidates include macOS arm64 and Linux arm64/x86-64 tools, with Linux
38
+ container images for both architectures. Windows uses the Linux CLI inside WSL2
39
+ (experimental);
40
+ Docker Desktop must expose its Linux engine to that distribution. Intel Mac is blocked
41
+ by the pinned upstream OpenShell release lacking a Darwin x86-64 executable.
42
+ See the [upstream support matrix](https://docs.nvidia.com/openshell/reference/support-matrix)
43
+ for host requirements. Setup validates the host,
37
44
  tools and ports; it pulls registry images by digest and downloads missing runtime tools
38
45
  when their definition supplies an HTTPS URL and SHA-256. It cannot recover a missing
39
46
  local development image ID. See the
@@ -66,15 +73,23 @@ silently replaced. Docker verifies image digests. No start command resolves late
66
73
  with an exact version, such as `0.1.0-alpha.1`. It:
67
74
 
68
75
  1. Runs checks and tests the compiled operator outside the checkout on macOS arm64.
69
- 2. Builds the pinned OpenClaw source and ClawScarf images on Linux arm64, checks
70
- runtime capabilities and the Chromium sandbox, and pushes candidate images to GHCR.
76
+ 2. Builds the pinned OpenClaw source and ClawScarf images on Linux arm64 and x86-64,
77
+ checks runtime capabilities and the Chromium sandbox, then starts a protected
78
+ installation with private LiteLLM networking and verifies stop/start retention.
79
+ It publishes image indexes containing both architectures to GHCR.
71
80
  3. Downloads checksum-pinned OpenShell tools, records the image digests, and assembles
72
- the runtime archive, individual executable assets, npm CLI, checksums and notices.
81
+ one runtime archive per host platform, individual executable assets, npm CLI,
82
+ checksums and notices.
73
83
  4. Installs the resulting npm archive into a temporary prefix and checks its CLI/catalog.
74
84
 
75
85
  The [image builder](../scripts/release/build-images.sh) and
76
86
  [candidate assembler](../scripts/release/candidate.ts) contain the build commands;
77
- Actions supplies runners and registry credentials. The source recipe pins the development
87
+ GitHub Actions orchestrates the build and supplies registry credentials. Image builds
88
+ and Linux installation checks use Depot ARM64/x86-64 runners; macOS checks and packaging
89
+ use GitHub-hosted runners. The Depot Managed Runners app connects the ClawScarf organization
90
+ to the RAW Labs Depot organization. The image job installs Docker 29.5.3 on its
91
+ disposable runner for the network features required by installations.
92
+ The source recipe pins the development
78
93
  runtime. Packaging resolves that same runtime to the candidate's immutable definition;
79
94
  it does not make recipes select latest. The first release uses the same CLI/runtime
80
95
  version. Recipe versions remain independent; bump them when defaults, pack selection
@@ -92,20 +107,33 @@ GitHub Release and publishes the already-built npm tarball. It never rebuilds im
92
107
  or packages. Prereleases use npm's `next` tag; stable versions use `latest`. Repeating
93
108
  a GitHub upload is allowed only when the existing checksums match exactly.
94
109
 
95
- Repository setup before first publication:
110
+ Repository setup:
96
111
 
97
112
  - GHCR image packages must be publicly readable; a private candidate digest is not
98
113
  usable by an unauthenticated installer. The build uses GitHub's scoped job token.
99
- - Configure the GitHub `release` environment and npm trusted publishing for
100
- `@clawscarf/cli`, repository `clawscarf/clawscarf`, workflow [publish-release.yml](../.github/workflows/publish-release.yml),
101
- environment `release`. npm ownership/initial package setup is separate from code.
102
- - Check transitive notices/source obligations before distributing the first images.
114
+ - The GitHub `release` environment restricts deployment to `main`. The npm
115
+ organization `@clawscarf` owns the public `@clawscarf/cli` package.
116
+ - npm trusted publishing is configured for repository `clawscarf/clawscarf`, workflow
117
+ [publish-release.yml](../.github/workflows/publish-release.yml), environment `release`.
118
+ The first tested tarball was published using the owner's authenticated npm CLI because
119
+ npm requires a package to exist before configuring trust. Subsequent publication
120
+ uses the workflow's short-lived identity rather than a stored npm token. That next
121
+ workflow publication has not yet been exercised.
122
+ - Transitive notices/source review remains open in [TODO.md](../TODO.md).
103
123
  Debian copyright files remain in the images; runtime assets include upstream notices
104
124
  and the image build exports its network-tool sources. Those files are not a claim of
105
125
  a completed license audit. Pinned Debian packages still depend on mirror retention.
106
126
 
107
- No npm/GitHub release has been published by adding these workflows. The first Actions
108
- run, clean-machine journey, registry access and publication remain in [TODO.md](../TODO.md).
127
+ [Candidate 35524428880](https://github.com/clawscarf/clawscarf/actions/runs/35524428880)
128
+ passed its checks, compiled archive test and all eight image builds. The same artifacts
129
+ are available in the [0.1.0-alpha.1 GitHub prerelease](https://github.com/clawscarf/clawscarf/releases/tag/v0.1.0-alpha.1).
130
+ The identical CLI tarball is published as `@clawscarf/cli@0.1.0-alpha.1`, tagged `next`.
131
+ All eight GHCR images were verified anonymously readable by digest. The packaged CLI
132
+ passed fresh setup on the development Mac, staging hosted administrator login, real
133
+ GPT-6 Astra inference and stop/start with retained chat history. Published tool URLs
134
+ passed download and SHA-256 verification. Disposable installation containers, volumes
135
+ and networks were deleted afterward. This does not establish a newly provisioned
136
+ host, production first-time signup, Linux/WSL or changed-version upgrades.
109
137
 
110
138
  ## Assemble runtime artifacts
111
139
 
@@ -12,12 +12,28 @@
12
12
  "darwin-arm64": {
13
13
  "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-aarch64-apple-darwin.tar.gz",
14
14
  "sha256": "e582f2374053bebac8e6aaeb4a369931b7d4bb97bd55055e2c02e85502627e22"
15
+ },
16
+ "linux-arm64": {
17
+ "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-aarch64-unknown-linux-musl.tar.gz",
18
+ "sha256": "7a949c48d1e000cd280869eea1e203e24816b9cfefc575b68a8b72b939cb3f43"
19
+ },
20
+ "linux-x64": {
21
+ "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-x86_64-unknown-linux-musl.tar.gz",
22
+ "sha256": "4fb4476d80a1875a0b83547ec3aba999cf0a2e2d75f95f2f709b622e2103520e"
15
23
  }
16
24
  },
17
25
  "gateway": {
18
26
  "darwin-arm64": {
19
27
  "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-gateway-aarch64-apple-darwin.tar.gz",
20
28
  "sha256": "f192d3d737c125264e13ef73458541df2ca6a9eb2fa599736a7f2587d5d2ce8d"
29
+ },
30
+ "linux-arm64": {
31
+ "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz",
32
+ "sha256": "292c379193a339220234ffea585350901468bb8f4076e2076bc074e8ed18974b"
33
+ },
34
+ "linux-x64": {
35
+ "url": "https://github.com/NVIDIA/OpenShell/releases/download/v0.0.116/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz",
36
+ "sha256": "59c6da724eae7a00c28826f9191efbdf4fbaa5c768afdc8dea6a80a949ebcc89"
21
37
  }
22
38
  },
23
39
  "gatewayImage": "ghcr.io/nvidia/openshell/gateway:0.0.116@sha256:05cf77bbb022a739aed6f22daa0e7e164415f4ab273b5f84319e46d91eb8f645"
@@ -9,6 +9,17 @@ registry images by digest. Local development image IDs cannot be downloaded. The
9
9
  package is named `@clawscarf/cli` and exposes the `clawscarf` command. Development archives
10
10
  remain private; the release candidate builder makes the package publishable. [Release contents](README.md) defines the publication model.
11
11
 
12
+ ## Install from npm
13
+
14
+ ```sh
15
+ npm install -g @clawscarf/cli@next
16
+ clawscarf configure
17
+ ```
18
+
19
+ The `next` tag selects the current alpha. Use an exact version instead when needed.
20
+ npm installs the CLI dependencies; runtime images and tools are fetched during setup.
21
+ See [release contents](README.md) for supported platforms and verification.
22
+
12
23
  ## Run the archive
13
24
 
14
25
  Verify the archive against its accompanying `SHA256SUMS`, then extract it into a new
@@ -28,7 +39,8 @@ Dependencies are installed from the included frozen lockfile. Its root importer
28
39
  contains only the production dependencies referenced by the staged operator;
29
40
  publisher and browser dependencies are omitted. Locked transitive versions are retained.
30
41
  No TypeScript compiler, contributor source or build step is needed to run the commands.
31
- The current local assembly supports macOS arm64 with Docker Desktop only.
42
+ Platform support is determined by the selected runtime definition. Published candidates
43
+ include macOS arm64 and Linux arm64/x86-64 tools; Windows runs the Linux CLI inside WSL2.
32
44
 
33
45
  The installer reviews recipe settings before credentials, prepares the installation,
34
46
  and offers **Start now**. It starts Docker services and protected OpenShell containers, then exits; closing
@@ -0,0 +1,63 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.1.0-alpha.2",
4
+ "sourceRevision": "dc08bf392bad89efabce65bdd56e93c508553203",
5
+ "platforms": [
6
+ "darwin-arm64",
7
+ "linux-arm64",
8
+ "linux-x64"
9
+ ],
10
+ "images": {
11
+ "postgres": "postgres@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193",
12
+ "gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:01105abab6d0b6330b55022187dd3367d552c2b66f6ddca5c0bb0279740a9bd3",
13
+ "companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:1cb04a8c4ec5a65c74095b54dfa63a0154621158348f7e8315b30e0679d8a3d0",
14
+ "openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:9aef63fd71cf55b8eb3971db1f49f2f5c9a467f782b699059f599dce2ea3beb4",
15
+ "relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:a80196a2dcc9a7835947ba69bd75c7e704db1b5cedac28bbc8670a675eeb5dec",
16
+ "models": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf",
17
+ "browser": {
18
+ "chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:6c8e0302ad8ce118440f4794772149cf4a621e9fa9d580492e24269efcc7618d",
19
+ "node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:47c9f847167eb8a9956b600f4f97310ede7428833f60bdbacfd3c0f91fee7da6",
20
+ "dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:f83d0bc00f8f71d6d809b5057eb5edbf4573b5e1565a8292dbe36021802c4c0b",
21
+ "egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:a863dcfe890596aa8824d2c35e503afe5ddeeb763d3eef83ce582eb4ea6fa595"
22
+ }
23
+ },
24
+ "tools": {
25
+ "openshell": {
26
+ "version": "0.0.116",
27
+ "cli": {
28
+ "darwin-arm64": {
29
+ "file": "tools/darwin-arm64/openshell",
30
+ "sha256": "0baeeffe0e4c184b08fde910ac9e8815fb0b8cfb808e49c0e634d7db65fb03bb",
31
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-darwin-arm64"
32
+ },
33
+ "linux-arm64": {
34
+ "file": "tools/linux-arm64/openshell",
35
+ "sha256": "84df9f059b16b66c83748606cc87d9edcda84b57effea19543f5c368aba7eb18",
36
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-linux-arm64"
37
+ },
38
+ "linux-x64": {
39
+ "file": "tools/linux-x64/openshell",
40
+ "sha256": "24e85062073d512d1951c76cd3890b7bce1ab01cc36a08f88b3b43c067a402da",
41
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-linux-x64"
42
+ }
43
+ },
44
+ "gateway": {
45
+ "darwin-arm64": {
46
+ "file": "tools/darwin-arm64/openshell-gateway",
47
+ "sha256": "298706259ef18350f334c222e981e91583558511d50bce22727c689d261203e6",
48
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-gateway-darwin-arm64"
49
+ },
50
+ "linux-arm64": {
51
+ "file": "tools/linux-arm64/openshell-gateway",
52
+ "sha256": "0e660428e655e7481f34ba8a4b351554400baf791863d32555c98dddb3f09ae2",
53
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-gateway-linux-arm64"
54
+ },
55
+ "linux-x64": {
56
+ "file": "tools/linux-x64/openshell-gateway",
57
+ "sha256": "780fc7ad871e2163f3fb866a2d0254ee682fc0d83e6261a84b350c8dcdd9f47b",
58
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.2/openshell-gateway-linux-x64"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
@@ -11,6 +11,7 @@ const settingsSchema = z.strictObject({
11
11
  port: z.number().int().min(1024).max(65535),
12
12
  gateway: z.string().min(1),
13
13
  cli: z.string().min(1),
14
+ hostGatewayIp: z.ipv4().optional(),
14
15
  });
15
16
  const releaseInput = JSON.parse(await readFile(new URL("../release/components.json", import.meta.url), "utf8"));
16
17
  const release = z
@@ -75,6 +76,7 @@ gateway_id = ${JSON.stringify(settings.name)}
75
76
  ttl_secs = 0
76
77
  [openshell.drivers.docker]
77
78
  network_name = ${JSON.stringify(settings.name)}
79
+ ${settings.hostGatewayIp ? `host_gateway_ip = ${JSON.stringify(settings.hostGatewayIp)}` : ""}
78
80
  sandbox_namespace = ${JSON.stringify(settings.name)}
79
81
  supervisor_image = ${JSON.stringify(release.supervisorImage)}
80
82
  image_pull_policy = "IfNotPresent"
@@ -91,6 +93,7 @@ command
91
93
  .requiredOption("--directory <path>", "New private controller directory")
92
94
  .requiredOption("--gateway <path>", "Verified openshell-gateway executable")
93
95
  .requiredOption("--cli <path>", "Verified openshell executable")
96
+ .option("--host-gateway-ip <address>", "Controller address on the runtime bridge")
94
97
  .option("--name <name>", "Isolated controller name", "clawscarf")
95
98
  .option("--port <port>", "Loopback controller port", "17671")
96
99
  .action(async (options) => {
@@ -101,6 +104,7 @@ command
101
104
  port: Number(options.port),
102
105
  gateway: resolve(options.gateway),
103
106
  cli: resolve(options.cli),
107
+ hostGatewayIp: options.hostGatewayIp,
104
108
  });
105
109
  await verifyExecutable(settings.gateway);
106
110
  await verifyExecutable(settings.cli);
@@ -115,6 +119,8 @@ command
115
119
  "localhost",
116
120
  "--server-san",
117
121
  "host.openshell.internal",
122
+ "--server-san",
123
+ "controller.clawscarf.internal",
118
124
  ], { timeout: 30000 });
119
125
  const mtls = join(directory, "config/openshell/gateways", settings.name, "mtls");
120
126
  await mkdir(mtls, { recursive: true, mode: 0o700 });
@@ -41,7 +41,6 @@ export function browserNodeServices(state, directory, browserAddress, prepared)
41
41
  image: state.input.relayImage,
42
42
  user: String(process.getuid?.() ?? 1000),
43
43
  networks: { machine: { ipv4_address: addresses.ingress }, default: {} },
44
- extra_hosts: ["host.docker.internal:host-gateway"],
45
44
  environment: {
46
45
  CLAWSCARF_NODE_INGRESS_ADDRESS: addresses.ingress,
47
46
  CLAWSCARF_NODE_GATEWAY_PORT: String(state.input.ports.native),
@@ -64,7 +64,7 @@ export async function ensureCertificates(directory) {
64
64
  "-subj",
65
65
  "/CN=ClawScarf local management",
66
66
  "-addext",
67
- "subjectAltName=DNS:localhost,DNS:host.docker.internal,IP:127.0.0.1",
67
+ "subjectAltName=DNS:localhost,DNS:host.docker.internal,DNS:models.clawscarf.internal,IP:127.0.0.1",
68
68
  "-addext",
69
69
  "basicConstraints=critical,CA:TRUE",
70
70
  ]);
@@ -6,7 +6,7 @@ import { resourceNames } from "./state.js";
6
6
  import { run } from "./process.js";
7
7
  import { modelGatewayServices } from "./model-gateway-compose.js";
8
8
  import { postgresImage } from "./images.js";
9
- export function composeConfiguration(state, directory, browserAddress, browserMachine) {
9
+ export function composeConfiguration(state, directory, controllerAddress, browserAddress, browserMachine, controllerSocketGroup = 0) {
10
10
  const { input, ownerId } = state;
11
11
  const names = resourceNames(state);
12
12
  const privateDirectory = join(directory, "private");
@@ -23,7 +23,7 @@ export function composeConfiguration(state, directory, browserAddress, browserMa
23
23
  const configuration = {
24
24
  name: names.project,
25
25
  services: {
26
- ...controllerServices(directory, state),
26
+ ...controllerServices(directory, state, controllerAddress, controllerSocketGroup),
27
27
  ...modelGatewayServices(directory, state),
28
28
  ...(input.browser && browserAddress && browserMachine
29
29
  ? browserNodeServices(state, directory, browserAddress, browserMachine)
@@ -153,9 +153,7 @@ export function composeConfiguration(state, directory, browserAddress, browserMa
153
153
  },
154
154
  },
155
155
  networks: {
156
- ...(input.relayImage
157
- ? { runtime: { external: true, name: names.sandbox } }
158
- : {}),
156
+ runtime: { external: true, name: names.sandbox },
159
157
  default: { external: true, name: `${names.project}_default` },
160
158
  ...(input.browser
161
159
  ? {
@@ -187,8 +187,8 @@ export const localInput = z
187
187
  export function parseLocalInput(value) {
188
188
  return localInput.parse(value);
189
189
  }
190
- export function managementOrigin(input) {
191
- return `https://host.docker.internal:${String(input.ports.management)}`;
190
+ export function managementOrigin() {
191
+ return "https://localhost:18801";
192
192
  }
193
193
  function mountedPrivateFile(directory, path, name) {
194
194
  absolutePath.parse(path);
@@ -224,10 +224,10 @@ export function generateLocalConfiguration(options) {
224
224
  port: 18801,
225
225
  },
226
226
  runtime: {
227
- origin: `http://host.docker.internal:${String(input.ports.native)}`,
228
- managementOrigin: managementOrigin(input),
227
+ origin: `http://application:${String(input.ports.native)}`,
228
+ managementOrigin: managementOrigin(),
229
229
  widgetOrigin,
230
- widgetUpstream: `http://host.docker.internal:${String(input.ports.nativeWidgets)}`,
230
+ widgetUpstream: `http://widgets:${String(input.ports.nativeWidgets)}`,
231
231
  },
232
232
  ...(input.team.certificateFile
233
233
  ? {
@@ -2,13 +2,14 @@ import { join } from "node:path";
2
2
  import { openshellGatewayImage } from "./images.js";
3
3
  import { resourceNames } from "./state.js";
4
4
  /** OpenShell alone receives Docker authority. Forwarders receive only client credentials. */
5
- export function controllerServices(directory, state) {
5
+ export function controllerServices(directory, state, address, socketGroup) {
6
6
  const controller = join(directory, "controller");
7
7
  const { input } = state;
8
8
  const name = resourceNames(state).sandbox;
9
9
  const port = input.ports.controller;
10
10
  const forward = (target, port) => ({
11
11
  image: input.openshellClientImage,
12
+ user: `${String(process.getuid?.() ?? 1000)}:${String(process.getgid?.() ?? 1000)}`,
12
13
  init: true,
13
14
  read_only: true,
14
15
  cap_drop: ["ALL"],
@@ -21,23 +22,31 @@ export function controllerServices(directory, state) {
21
22
  XDG_DATA_HOME: "/tmp/data",
22
23
  },
23
24
  volumes: [`${join(controller, "config")}:/controller/config:ro`],
24
- extra_hosts: ["host.openshell.internal:host-gateway"],
25
25
  command: [
26
26
  "forward",
27
- "start",
27
+ "service",
28
+ "--target-port",
29
+ String(port),
30
+ "--local",
28
31
  `0.0.0.0:${String(port)}`,
29
32
  target,
30
33
  "--gateway",
31
34
  name,
32
35
  "--gateway-endpoint",
33
- `https://host.openshell.internal:${String(input.ports.controller)}`,
36
+ `https://controller.clawscarf.internal:${String(input.ports.controller)}`,
34
37
  ],
35
38
  ports: [`127.0.0.1:${String(port)}:${String(port)}`],
36
39
  });
37
40
  return {
38
41
  controller: {
39
42
  image: openshellGatewayImage,
40
- user: "0:0",
43
+ user: `${String(process.getuid?.() ?? 1000)}:${String(process.getgid?.() ?? 1000)}`,
44
+ // Desktop exposes a root-group socket; Linux Engine uses the host socket's group.
45
+ group_add: [...new Set(["0", String(socketGroup)])],
46
+ networks: {
47
+ default: { aliases: ["controller.clawscarf.internal"] },
48
+ runtime: { ipv4_address: address },
49
+ },
41
50
  command: ["--config", join(controller, "gateway.toml")],
42
51
  environment: {
43
52
  XDG_CONFIG_HOME: join(controller, "config"),
@@ -51,7 +60,6 @@ export function controllerServices(directory, state) {
51
60
  "/var/run/docker.sock:/var/run/docker.sock",
52
61
  ],
53
62
  ports: [`127.0.0.1:${String(port)}:${String(port)}`],
54
- extra_hosts: ["host.openshell.internal:host-gateway"],
55
63
  },
56
64
  application: forward(name, input.ports.native),
57
65
  widgets: forward(name, input.ports.nativeWidgets),
@@ -28,6 +28,12 @@ export function modelGatewayServices(directory, state) {
28
28
  },
29
29
  models: {
30
30
  image: input.image,
31
+ user: `${String(process.getuid?.() ?? 1000)}:${String(process.getgid?.() ?? 1000)}`,
32
+ environment: { HOME: "/tmp" },
33
+ networks: {
34
+ default: {},
35
+ runtime: { aliases: ["models.clawscarf.internal"] },
36
+ },
31
37
  init: true,
32
38
  read_only: true,
33
39
  cap_drop: ["ALL"],
@@ -43,6 +43,14 @@ export async function prepareModelGateway(directory, state, options = {}) {
43
43
  return;
44
44
  const loaded = await loadGatewayConfiguration(input.configurationFile, input.upstreamEnvironmentFile);
45
45
  const root = join(directory, "private/models");
46
+ let baseUrl = "https://models.clawscarf.internal:4000/v1";
47
+ if (options.replaceConfiguration) {
48
+ const current = configurationSchema.parse(JSON.parse(await readFile(join(root, "native.json"), "utf8")));
49
+ if (current.mode !== "litellm")
50
+ throw new LocalSetupError("invalid_model_setup", "Expected an existing model gateway.");
51
+ // Model choices do not change the installation's retained network endpoint.
52
+ baseUrl = current.baseUrl;
53
+ }
46
54
  await mkdir(root, { recursive: true, mode: 0o700 });
47
55
  for (const [name, prefix] of [
48
56
  ["master-key", "sk-"],
@@ -74,7 +82,7 @@ export async function prepareModelGateway(directory, state, options = {}) {
74
82
  await publish(join(root, "models.json"), JSON.stringify(liteLlmConfiguration(loaded.configuration)));
75
83
  await publish(join(root, "native.json"), JSON.stringify({
76
84
  ...loaded.configuration,
77
- baseUrl: `https://host.docker.internal:${String(input.port)}/v1`,
85
+ baseUrl,
78
86
  }));
79
87
  }
80
88
  export async function prepareModelCredential(directory, state) {
@@ -264,6 +264,10 @@ export async function ensureLocalNetworks(directory, state, command = run) {
264
264
  "create",
265
265
  "--driver",
266
266
  "bridge",
267
+ // Docker chooses a free subnet; declaring its size permits static endpoints.
268
+ ...(intent.purpose !== "companion"
269
+ ? ["--subnet", "0.0.0.0/24"]
270
+ : []),
267
271
  ...(intent.purpose === "runtime" ? ["--attachable"] : []),
268
272
  ...(["browser", "machine"].includes(intent.purpose)
269
273
  ? [
@@ -352,3 +356,20 @@ export async function observedBrowserMachineAddresses(directory, state, command
352
356
  changed();
353
357
  return { node: observed.isolated.address, ingress, dns };
354
358
  }
359
+ /** Stable address on the owned runtime bridge for the containerized OpenShell controller. */
360
+ export async function observedControllerAddress(directory, state) {
361
+ await verifyLocalNetworks(directory, state);
362
+ const configs = z
363
+ .array(z.object({ Subnet: z.string(), Gateway: z.string() }))
364
+ .parse(JSON.parse(await run("docker", [
365
+ "network",
366
+ "inspect",
367
+ resourceNames(state).sandbox,
368
+ "--format",
369
+ "{{json .IPAM.Config}}",
370
+ ])));
371
+ const config = configs.find((item) => isIP(item.Gateway) === 4);
372
+ if (!config)
373
+ changed();
374
+ return reservedAddress(config).address;
375
+ }
@@ -8,10 +8,10 @@ import { prepareRelay } from "./relay.js";
8
8
  import { prepareRuntimePolicy } from "./policy.js";
9
9
  import { prepareBrowser, initializeBrowserVolume } from "./browser.js";
10
10
  import { readTeamMaterials, prepareTeamFiles } from "./team.js";
11
- import { readFile, lstat } from "node:fs/promises";
11
+ import { readFile, lstat, stat } from "node:fs/promises";
12
12
  import { join, resolve } from "node:path";
13
13
  import { prepareInitialModels, withInitialModels, } from "./models.js";
14
- import { ensureLocalNetworks } from "./networks.js";
14
+ import { ensureLocalNetworks, observedControllerAddress } from "./networks.js";
15
15
  import { nodeEntrypoint } from "./entrypoint.js";
16
16
  import { ensureCertificates } from "./certificates.js";
17
17
  import { z } from "zod";
@@ -29,8 +29,9 @@ export async function prepareLocal(directoryInput, inputValue, capabilities) {
29
29
  const input = parseLocalInput(inputValue);
30
30
  const connections = await loadInitialConnections(input.connections);
31
31
  const teamMaterials = await readTeamMaterials(input.team);
32
- if (process.platform !== "darwin" || process.arch !== "arm64")
33
- throw new LocalSetupError("platform_unqualified", "This local assembly currently requires macOS arm64 with Docker Desktop; other platforms need qualification.");
32
+ if (!["darwin", "linux"].includes(process.platform) ||
33
+ !["arm64", "x64"].includes(process.arch))
34
+ throw new LocalSetupError("platform_unqualified", "Use macOS or Linux (including WSL2) with Linux Docker containers.");
34
35
  for (const image of [
35
36
  input.runtimeImage,
36
37
  input.openshellClientImage,
@@ -71,6 +72,7 @@ export async function prepareLocal(directoryInput, inputValue, capabilities) {
71
72
  const connectionsEndpoint = await prepareInitialConnections(directory, connections);
72
73
  await prepareModelGateway(directory, state);
73
74
  await ensureLocalNetworks(directory, state);
75
+ const controllerAddress = await observedControllerAddress(directory, state);
74
76
  const browser = await prepareBrowser(directory, state);
75
77
  const browserMachine = browser
76
78
  ? await prepareBrowserNode(directory, state, browser.token)
@@ -86,7 +88,9 @@ export async function prepareLocal(directoryInput, inputValue, capabilities) {
86
88
  await ensureOwnedVolume(names.browserVolume, state.ownerId);
87
89
  await initializeBrowserVolume(state, browser.token);
88
90
  }
89
- await ensurePrivateFile(join(directory, "compose.json"), JSON.stringify(composeConfiguration(state, directory, browser?.address, browserMachine), null, 2));
91
+ await ensurePrivateFile(join(directory, "compose.json"), JSON.stringify(composeConfiguration(state, directory, controllerAddress, browser?.address, browserMachine, process.platform === "linux"
92
+ ? (await stat("/var/run/docker.sock")).gid
93
+ : 0), null, 2));
90
94
  await prepareModelCredential(directory, state);
91
95
  const models = await prepareInitialModels(directory, input.models);
92
96
  await prepareRuntimePolicy(directory, models, connectionsEndpoint);
@@ -151,6 +155,8 @@ export async function prepareLocal(directoryInput, inputValue, capabilities) {
151
155
  input.openshellGateway,
152
156
  "--cli",
153
157
  input.openshellCli,
158
+ "--host-gateway-ip",
159
+ controllerAddress,
154
160
  "--name",
155
161
  names.sandbox,
156
162
  "--port",
@@ -66,7 +66,11 @@ export const terminalPrompts = {
66
66
  note: clack.note,
67
67
  async openBrowser(url) {
68
68
  try {
69
- await promisify(execFile)(process.platform === "darwin" ? "open" : "xdg-open", [url], { timeout: 5000 });
69
+ await promisify(execFile)(process.platform === "darwin"
70
+ ? "open"
71
+ : process.env.WSL_DISTRO_NAME
72
+ ? "explorer.exe"
73
+ : "xdg-open", [url], { timeout: 5000 });
70
74
  }
71
75
  catch {
72
76
  process.stderr.write("Could not open a browser. Open the link above manually.\n");
@@ -12,8 +12,9 @@ import { acquireRuntimeTools } from "./runtime.js";
12
12
  import { InstallationError } from "./errors.js";
13
13
  /** No configuration, credentials or cloud account is needed for these checks. */
14
14
  export async function checkHost(command = run) {
15
- if (process.platform !== "darwin" || process.arch !== "arm64")
16
- throw new InstallationError("unsupported_platform", "This release requires macOS on Apple Silicon with Docker Desktop.");
15
+ if (!["darwin", "linux"].includes(process.platform) ||
16
+ !["arm64", "x64"].includes(process.arch))
17
+ throw new InstallationError("unsupported_platform", "Use macOS or Linux (including WSL2), with an ARM64 or x86-64 processor and Linux Docker containers. Run the Windows CLI inside WSL2.");
17
18
  let os;
18
19
  try {
19
20
  os = await command("docker", ["info", "--format", "{{.OSType}}"]);
@@ -23,8 +24,8 @@ export async function checkHost(command = run) {
23
24
  error.commandFailure?.reason === "spawn" &&
24
25
  error.commandFailure.systemCode === "ENOENT";
25
26
  throw new InstallationError("unavailable", missing
26
- ? "Docker is not installed or is not on PATH. Install Docker Desktop, start it, then run configure again."
27
- : "Cannot reach Docker. Start Docker Desktop and wait until it is running, then try again.");
27
+ ? "Docker is not installed or is not on PATH. Install Docker Engine with Compose or Docker Desktop, then run configure again."
28
+ : "Cannot reach Docker. Start Docker and check that your user can access its socket, then try again.");
28
29
  }
29
30
  if (os.trim() !== "linux")
30
31
  throw new InstallationError("unsupported_platform", "Docker must run Linux containers.");
@@ -32,8 +33,12 @@ export async function checkHost(command = run) {
32
33
  await command("docker", ["compose", "version"]);
33
34
  }
34
35
  catch {
35
- throw new InstallationError("unavailable", "Docker Compose is unavailable. Install or update Docker Desktop, then try again.");
36
+ throw new InstallationError("unavailable", "Docker Compose is unavailable. Install the Docker Compose plugin or update Docker Desktop, then try again.");
36
37
  }
38
+ const version = (await command("docker", ["version", "--format", "{{.Server.Version}}"])).trim();
39
+ const major = Number(/^([0-9]+)\./.exec(version)?.[1]);
40
+ if (!Number.isInteger(major) || major < 29)
41
+ throw new InstallationError("unsupported_platform", "Docker Engine 29 or newer is required for automatic subnet allocation with fixed service addresses. Update Docker Engine or Docker Desktop, then try again.");
37
42
  }
38
43
  /** Stream only Docker's layer progress, never registry error payloads or credentials. */
39
44
  export function pullImage(image, report, signal) {
@@ -3,7 +3,7 @@ import { hostedOidc, hostedConnections } from "../cloud/registration.js";
3
3
  import { dirname, resolve, join } from "node:path";
4
4
  import { loadGatewayConfiguration } from "../deployment/model-gateway.js";
5
5
  import { createServer } from "node:net";
6
- import { releaseSchema } from "../release/definition.js";
6
+ import { releaseSchema, releaseTools } from "../release/definition.js";
7
7
  import { parseLocalInput, } from "../deployment/configuration.js";
8
8
  import { loadInitialModels } from "../deployment/models.js";
9
9
  import { loadInitialConnections, readInitialConnectionToken, } from "../deployment/connections.js";
@@ -49,10 +49,11 @@ export async function resolveInstallation(configFile, internalPorts) {
49
49
  if (!release.platforms.some((platform) => platform === `${process.platform}-${process.arch}`))
50
50
  throw new InstallationError("unsupported_platform", "This release does not support this host platform.");
51
51
  const toolBase = dirname(releasePath);
52
- const cli = resolve(toolBase, release.tools.openshell.cli.file);
53
- const gateway = resolve(toolBase, release.tools.openshell.gateway.file);
54
- await verifyReleaseTool(cli, release.tools.openshell.cli.sha256);
55
- await verifyReleaseTool(gateway, release.tools.openshell.gateway.sha256);
52
+ const tools = releaseTools(release);
53
+ const cli = resolve(toolBase, tools.cli.file);
54
+ const gateway = resolve(toolBase, tools.gateway.file);
55
+ await verifyReleaseTool(cli, tools.cli.sha256);
56
+ await verifyReleaseTool(gateway, tools.gateway.sha256);
56
57
  const [controller, management, native, nativeWidgets, database, browser, models,] = internalPorts;
57
58
  const inputs = {};
58
59
  async function inputFile(value, secret) {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmod, copyFile, mkdir, open, rename, rm, writeFile, } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { releaseSchema } from "../release/definition.js";
4
+ import { releaseSchema, releaseTools } from "../release/definition.js";
5
5
  import { readJson, verifyReleaseTool } from "./files.js";
6
6
  import { InstallationError } from "./errors.js";
7
7
  /** Installation-owned paths survive npm cache cleanup and later CLI updates. */
@@ -10,7 +10,7 @@ export async function retainRuntime(source, directory) {
10
10
  const target = join(directory, "runtime");
11
11
  await mkdir(join(target, "tools"), { recursive: true, mode: 0o700 });
12
12
  for (const name of ["cli", "gateway"]) {
13
- const tool = release.tools.openshell[name];
13
+ const tool = releaseTools(release)[name];
14
14
  const original = resolve(dirname(source), tool.file);
15
15
  tool.file = `tools/openshell${name === "gateway" ? "-gateway" : ""}`;
16
16
  try {
@@ -34,7 +34,7 @@ export async function acquireRuntimeTools(releaseFile, options) {
34
34
  const release = releaseSchema.parse(await readJson(releaseFile));
35
35
  for (const name of ["cli", "gateway"]) {
36
36
  options.signal?.throwIfAborted();
37
- const tool = release.tools.openshell[name];
37
+ const tool = releaseTools(release)[name];
38
38
  const file = resolve(dirname(releaseFile), tool.file);
39
39
  try {
40
40
  await verifyReleaseTool(file, tool.sha256);
@@ -15,12 +15,18 @@ const file = z.strictObject({
15
15
  }, "Tool downloads require HTTPS without credentials.")
16
16
  .optional(),
17
17
  });
18
+ export const hostPlatformSchema = z.enum([
19
+ "darwin-arm64",
20
+ "linux-arm64",
21
+ "linux-x64",
22
+ ]);
23
+ const tool = z.union([file, z.partialRecord(hostPlatformSchema, file)]);
18
24
  /** Pinned runtime metadata, independent of recipe defaults and deployment state. */
19
25
  export const releaseSchema = z.strictObject({
20
26
  schemaVersion: z.literal(1),
21
27
  version: z.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
22
28
  sourceRevision: z.string().regex(/^[a-f0-9]{40}$/),
23
- platforms: z.array(z.literal("darwin-arm64")).min(1).max(1),
29
+ platforms: z.array(hostPlatformSchema).min(1),
24
30
  images: z
25
31
  .strictObject({
26
32
  postgres: z.literal(postgresImage),
@@ -44,8 +50,29 @@ export const releaseSchema = z.strictObject({
44
50
  tools: z.strictObject({
45
51
  openshell: z.strictObject({
46
52
  version: z.string().min(1),
47
- cli: file,
48
- gateway: file,
53
+ cli: tool,
54
+ gateway: tool,
49
55
  }),
50
56
  }),
51
57
  });
58
+ /** Single-platform development bundles and multi-platform published bundles share resolution. */
59
+ export function releaseTools(release, platform = `${process.platform}-${process.arch}`) {
60
+ const host = hostPlatformSchema.parse(platform);
61
+ if (!release.platforms.includes(host))
62
+ throw new Error(`Runtime ${release.version} does not include ${host}.`);
63
+ const select = (value) => {
64
+ if ("file" in value) {
65
+ if (release.platforms.length !== 1)
66
+ throw new Error("Multi-platform runtimes require a tool for each platform.");
67
+ return value;
68
+ }
69
+ const selected = value[host];
70
+ if (!selected)
71
+ throw new Error(`Missing runtime tool for ${host}.`);
72
+ return selected;
73
+ };
74
+ return {
75
+ cli: select(release.tools.openshell.cli),
76
+ gateway: select(release.tools.openshell.gateway),
77
+ };
78
+ }
@@ -1,37 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "version": "0.1.0-alpha.1",
4
- "sourceRevision": "a53e95251321e61d8ffd6d06a6984f651c0b0db4",
5
- "platforms": [
6
- "darwin-arm64"
7
- ],
8
- "images": {
9
- "postgres": "postgres@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193",
10
- "gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:eaa25c8e487970176b95de657df8bf4faff605f9f265ca760197800d9a1e5fcf",
11
- "companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:97cfeb78ab56ba625b30aafe2249633d9b69f7f3c75de7116453385010e6281f",
12
- "openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:669f37f654926fc7714783cc8b1b76bae9666895c2a4da959580037d281ebf56",
13
- "relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:5ee13e9496e3b76e076e91c59a861a10ad7a31a13b145384fba314d7f0b686b5",
14
- "models": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf",
15
- "browser": {
16
- "chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:28ca9e4007d3b7c1bb9cb8f4bbaf275d1bc36ee2744079326b151b1f9eec78fd",
17
- "node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:2837d8b75df69fb39c7f7ca8fa68249d9de6e20d4e028213792de5f8f9ef1bd7",
18
- "dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:9f14cb85df522014c2bfc068e2b5788568f26b6e8d948cebe1250aff7630847b",
19
- "egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:bd819dffa4e1f60fbeed3b459a26b3b274300a4e5836bb90cbd080f6681d594c"
20
- }
21
- },
22
- "tools": {
23
- "openshell": {
24
- "version": "0.0.116",
25
- "cli": {
26
- "file": "tools/openshell",
27
- "sha256": "0baeeffe0e4c184b08fde910ac9e8815fb0b8cfb808e49c0e634d7db65fb03bb",
28
- "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.1/openshell-darwin-arm64"
29
- },
30
- "gateway": {
31
- "file": "tools/openshell-gateway",
32
- "sha256": "298706259ef18350f334c222e981e91583558511d50bce22727c689d261203e6",
33
- "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.1/openshell-gateway-darwin-arm64"
34
- }
35
- }
36
- }
37
- }