@clawscarf/cli 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +6 -1
- package/package.json +1 -1
- package/pnpm-lock.yaml +8 -0
- package/recipes/README.md +5 -3
- package/recipes/team-server/recipe.json +4 -4
- package/release/README.md +29 -12
- package/release/components.json +1 -1
- package/release/operator.md +1 -1
- package/runtime/{releases/0.1.0-alpha.6.json → current.json} +16 -16
- package/scripts/deployment/configuration.js +1 -0
- package/scripts/deployment/connection-settings.js +4 -1
- package/scripts/deployment/launch.js +3 -3
- package/scripts/deployment/network-policy.js +141 -0
- package/scripts/deployment/policy.js +7 -3
- package/scripts/deployment/prepare.js +1 -1
- package/scripts/deployment/public-web.js +222 -0
- package/scripts/installation/command.js +1 -1
- package/scripts/installation/configuration.js +1 -0
- package/scripts/installation/configure.js +1 -0
- package/scripts/installation/installer/collect.js +15 -0
- package/scripts/installation/installer/menu.js +2 -1
- package/scripts/installation/installer/prompts.js +1 -1
- package/scripts/installation/installer/run.js +17 -2
- package/scripts/installation/installer/settings.js +4 -1
- package/scripts/installation/installer/summary.js +1 -0
- package/scripts/installation/options.js +7 -1
- package/scripts/installation/recipes/definition.js +1 -0
- package/scripts/installation/reconfigure.js +10 -2
- package/scripts/installation/resolve.js +1 -0
- package/scripts/installation/setup.js +1 -0
- package/scripts/release/definition.js +9 -0
- package/scripts/deployment/connection-policy.js +0 -80
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ models, connected accounts, and execution protected by NVIDIA OpenShell.
|
|
|
13
13
|
## Get started
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
curl -fsSL https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
16
|
+
curl -fsSL https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/install.sh | sh
|
|
17
17
|
export PATH="$HOME/.local/bin:$PATH"
|
|
18
18
|
clawscarf configure
|
|
19
19
|
```
|
|
@@ -90,6 +90,11 @@ execution files and browser accounts. Native roles control application permissio
|
|
|
90
90
|
people allowed to run code share the Gateway’s authority. Use separate installations
|
|
91
91
|
for teams that must be isolated from one another.
|
|
92
92
|
|
|
93
|
+
The Team server recipe enables public web access for agents and shell tools. Operators
|
|
94
|
+
can turn it off; private destinations remain restricted to explicitly configured services.
|
|
95
|
+
Public web access permits sending team data to public services. See the
|
|
96
|
+
[network boundary](https://github.com/clawscarf/clawscarf/blob/main/deploy/openshell/README.md#public-web-access) for enforcement and limits.
|
|
97
|
+
|
|
93
98
|
Default login and optional Connections use **ClawScarf Cloud**. You can use your own
|
|
94
99
|
OIDC provider and disable Connections to run without those hosted services. Original
|
|
95
100
|
model-provider keys and Connections management credentials stay outside OpenClaw.
|
package/package.json
CHANGED
package/pnpm-lock.yaml
CHANGED
|
@@ -620,6 +620,12 @@ packages:
|
|
|
620
620
|
integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
'@types/semver@7.8.0':
|
|
624
|
+
resolution:
|
|
625
|
+
{
|
|
626
|
+
integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==
|
|
627
|
+
}
|
|
628
|
+
|
|
623
629
|
'@types/ws@8.18.1':
|
|
624
630
|
resolution:
|
|
625
631
|
{
|
|
@@ -2776,6 +2782,8 @@ snapshots:
|
|
|
2776
2782
|
|
|
2777
2783
|
'@types/retry@0.12.5': {}
|
|
2778
2784
|
|
|
2785
|
+
'@types/semver@7.8.0': {}
|
|
2786
|
+
|
|
2779
2787
|
'@types/ws@8.18.1':
|
|
2780
2788
|
dependencies:
|
|
2781
2789
|
'@types/node': 24.10.13
|
package/recipes/README.md
CHANGED
|
@@ -5,13 +5,15 @@ Recipes are bundled with the CLI. Each folder contains a [recipe.json](team-serv
|
|
|
5
5
|
|
|
6
6
|
```text
|
|
7
7
|
recipes/team-server/recipe.json
|
|
8
|
-
runtime/
|
|
8
|
+
runtime/current.json
|
|
9
9
|
packs/research-team/pack.json
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Each recipe has an ID and a version. Accepted installations retain that provenance
|
|
13
13
|
alongside their actual settings; a CLI update never reapplies new defaults. Bump the
|
|
14
|
-
recipe version when its defaults, pack selections or runtime
|
|
14
|
+
recipe version when its defaults, pack selections or explicit runtime reference change.
|
|
15
|
+
The resolved runtime release is retained separately; automatic advancement of the
|
|
16
|
+
[current selection](../release/README.md#published-and-development-use) does not change recipe defaults.
|
|
15
17
|
|
|
16
18
|
A recipe's `runtime` points to an exact runtime definition, relative to the recipe
|
|
17
19
|
file. Runtime images and tools are fixed; its other fields are defaults that users
|
|
@@ -21,7 +23,7 @@ no executable hooks, secrets or user identities.
|
|
|
21
23
|
|
|
22
24
|
- `models` selects model ID, provider and reasoning from the bundled
|
|
23
25
|
[model catalog](../deploy/models/catalog.json).
|
|
24
|
-
- `defaults` selects resources, browser and Connections enablement.
|
|
26
|
+
- `defaults` selects resources, public web access, browser and Connections enablement.
|
|
25
27
|
- `packs: [{ id, members }]` selects agents from the bundled [packs](../packs/README.md).
|
|
26
28
|
The installer checks members and collects required credentials after settings review.
|
|
27
29
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "team-server",
|
|
3
|
-
"version": "0.1.1",
|
|
4
|
-
"runtime": "../../runtime/releases/0.1.0-alpha.6.json",
|
|
5
3
|
"name": "Team server",
|
|
6
4
|
"description": "A protected OpenClaw server for your team, with hosted login, managed models and optional Connections.",
|
|
7
5
|
"maturity": "supported",
|
|
@@ -17,12 +15,14 @@
|
|
|
17
15
|
},
|
|
18
16
|
"connections": {
|
|
19
17
|
"enabled": true
|
|
20
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"publicWeb": true
|
|
21
20
|
},
|
|
22
21
|
"models": {
|
|
23
22
|
"model": "gpt-6-astra",
|
|
24
23
|
"provider": "openai",
|
|
25
24
|
"reasoning": "medium"
|
|
26
25
|
},
|
|
27
|
-
"
|
|
26
|
+
"runtime": "../../runtime/current.json",
|
|
27
|
+
"version": "0.1.3"
|
|
28
28
|
}
|
package/release/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
These have separate responsibilities:
|
|
4
4
|
|
|
5
|
-
| Definition | Owns | Location
|
|
6
|
-
| --------------- | ------------------------------------------------------------------------------------------------ |
|
|
7
|
-
| Runtime release | Exact software images, OpenShell executables, checksums and supported platforms | [runtime/
|
|
8
|
-
| Recipe | A fixed runtime reference plus editable model, reasoning, resource, capability and pack defaults | [recipes](../recipes/README.md)
|
|
9
|
-
| Pack | Native agent, skill and workflow files with declared prerequisites | [packs](../packs/README.md)
|
|
5
|
+
| Definition | Owns | Location |
|
|
6
|
+
| --------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
|
|
7
|
+
| Runtime release | Exact software images, OpenShell executables, checksums and supported platforms | [runtime/current.json](../runtime/current.json) |
|
|
8
|
+
| Recipe | A fixed runtime reference plus editable model, reasoning, resource, capability and pack defaults | [recipes](../recipes/README.md) |
|
|
9
|
+
| Pack | Native agent, skill and workflow files with declared prerequisites | [packs](../packs/README.md) |
|
|
10
10
|
|
|
11
11
|
Several recipes can use the same runtime. A runtime contains no recipes, model
|
|
12
12
|
catalog or pack selections. Changing a recipe does not require rebuilding its runtime.
|
|
@@ -20,10 +20,23 @@ Use the [installation guide](../deploy/deployment/installation.md) for published
|
|
|
20
20
|
CLI installation, platform requirements and runtime downloads. Use the
|
|
21
21
|
[development command](../scripts/README.md#development-command) for this checkout.
|
|
22
22
|
|
|
23
|
-
The tracked [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
The tracked [current runtime](../runtime/current.json) selects a published release
|
|
24
|
+
by exact image digests and downloadable tool checksums. The development command
|
|
25
|
+
reads it from the checkout; it does not query a remote latest version or build images.
|
|
26
|
+
New installations retain their own copy. Updating this file or the CLI never changes
|
|
27
|
+
an existing installation's saved selection.
|
|
28
|
+
|
|
29
|
+
After publication, the workflow copies the verified release manifest into this file
|
|
30
|
+
and commits it to `main`. Pulling that commit updates the development command's
|
|
31
|
+
selection. [Promotion](../scripts/release/advance-runtime.ts) requires a newer version
|
|
32
|
+
and the same selection the candidate was built against; an identical repeat is a no-op.
|
|
33
|
+
A changed pin or concurrent Git update fails visibly instead of overwriting another
|
|
34
|
+
selection. The promotion job can be retried independently of publication. Repository
|
|
35
|
+
rules must permit the release workflow's bot to push this commit to `main`.
|
|
36
|
+
|
|
37
|
+
For experiments with locally built images/tools, create a separate runtime bundle
|
|
38
|
+
and custom recipe using [runtime assembly](#assemble-runtime-artifacts). The bundled
|
|
39
|
+
recipe uses the same published artifact format in development and installed CLIs.
|
|
27
40
|
|
|
28
41
|
## Publishing structure
|
|
29
42
|
|
|
@@ -84,9 +97,10 @@ owns source identity and reconstruction. Candidate assembly packages
|
|
|
84
97
|
[openclaw-source.json and openclaw-patches.tgz](../scripts/release/candidate.ts)
|
|
85
98
|
with checksums; each runtime archive includes both. The runtime definition's `sourceRevision`
|
|
86
99
|
identifies the ClawScarf commit that owns those inputs.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
Recipes reference the current manifest within their checkout or installed package.
|
|
101
|
+
Candidate packaging replaces that file only in the staged package with the newly
|
|
102
|
+
built manifest. It leaves source files untouched; publication advances the checkout
|
|
103
|
+
as described above. Published packages and release assets remain immutable.
|
|
90
104
|
|
|
91
105
|
The `release-candidate` Actions artifact is the reviewable output. Download it and test
|
|
92
106
|
a fresh supported installation, administrator login, real inference and stop/start
|
|
@@ -99,6 +113,9 @@ build run from `main`. It checks the source commit and artifact checksums, creat
|
|
|
99
113
|
GitHub Release and publishes the already-built npm tarball. It never rebuilds images
|
|
100
114
|
or packages. Prereleases use npm's `next` tag; stable versions use `latest`. Repeating
|
|
101
115
|
a GitHub upload is allowed only when the existing checksums match exactly.
|
|
116
|
+
A separate dependent job then advances the checkout's current runtime selection;
|
|
117
|
+
a failure there leaves the published release available and can be retried without
|
|
118
|
+
republishing npm or rebuilding artifacts.
|
|
102
119
|
|
|
103
120
|
Repository setup:
|
|
104
121
|
|
package/release/components.json
CHANGED
package/release/operator.md
CHANGED
|
@@ -32,6 +32,6 @@ node scripts/clawscarf.js configure --directory /absolute/new-team
|
|
|
32
32
|
|
|
33
33
|
Keep installation data outside the extracted package. A development runtime that
|
|
34
34
|
references local image IDs needs those images prepared separately; see
|
|
35
|
-
[
|
|
35
|
+
[custom runtime assembly](README.md#assemble-runtime-artifacts).
|
|
36
36
|
The optional pack operator also needs its [Python dependencies](../packs/README.md#requirements-and-bindings).
|
|
37
37
|
Required [notices](../THIRD_PARTY_NOTICES.md) and dependency licenses accompany the payload.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"sourceRevision": "
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
|
+
"sourceRevision": "dc1a8fc480b3a55841e2a1c1aea58bad9811402c",
|
|
5
5
|
"platforms": [
|
|
6
6
|
"darwin-arm64",
|
|
7
7
|
"linux-arm64",
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
],
|
|
10
10
|
"images": {
|
|
11
11
|
"postgres": "postgres@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193",
|
|
12
|
-
"gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:
|
|
13
|
-
"companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:
|
|
14
|
-
"openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:
|
|
15
|
-
"relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:
|
|
12
|
+
"gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:a942812602fc1bdf40421d0ddcfda89aa87b9412b0616adba9a3da4e2a8a9a91",
|
|
13
|
+
"companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:fea91c1beda96cf811cfc952c77fd7ac17df8374704da926984ec65df8de49af",
|
|
14
|
+
"openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:419510bc89b4beee1763ad4b4a86ec490546e4cc1e645d054ef05a5cf654f27d",
|
|
15
|
+
"relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:602226e11f463fd811378173caedc0bea017f556d3fe94d17f4a752aaabd7171",
|
|
16
16
|
"models": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf",
|
|
17
17
|
"browser": {
|
|
18
|
-
"chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:
|
|
19
|
-
"node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:
|
|
20
|
-
"dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:
|
|
21
|
-
"egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:
|
|
18
|
+
"chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:ce259d28f078ac5d3ff9afc0a79b0a8935873eda953e6c04dbd8f756c3eb17ac",
|
|
19
|
+
"node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:de6b364dad0010ddf17f141888569c75f72364f4c2305d96e012c0c78c34f7c2",
|
|
20
|
+
"dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:3b8e9364ba8cc6271a53135c2c82944f9a3be423b20ccef3dea930e38d08d8a8",
|
|
21
|
+
"egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:06a33c3d9a5979359c92179ae936bfc0bb991b057eb5a4dc87dcf9240b513542"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"tools": {
|
|
@@ -28,34 +28,34 @@
|
|
|
28
28
|
"darwin-arm64": {
|
|
29
29
|
"file": "tools/darwin-arm64/openshell",
|
|
30
30
|
"sha256": "0baeeffe0e4c184b08fde910ac9e8815fb0b8cfb808e49c0e634d7db65fb03bb",
|
|
31
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
31
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-darwin-arm64"
|
|
32
32
|
},
|
|
33
33
|
"linux-arm64": {
|
|
34
34
|
"file": "tools/linux-arm64/openshell",
|
|
35
35
|
"sha256": "84df9f059b16b66c83748606cc87d9edcda84b57effea19543f5c368aba7eb18",
|
|
36
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
36
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-linux-arm64"
|
|
37
37
|
},
|
|
38
38
|
"linux-x64": {
|
|
39
39
|
"file": "tools/linux-x64/openshell",
|
|
40
40
|
"sha256": "24e85062073d512d1951c76cd3890b7bce1ab01cc36a08f88b3b43c067a402da",
|
|
41
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
41
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-linux-x64"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"gateway": {
|
|
45
45
|
"darwin-arm64": {
|
|
46
46
|
"file": "tools/darwin-arm64/openshell-gateway",
|
|
47
47
|
"sha256": "298706259ef18350f334c222e981e91583558511d50bce22727c689d261203e6",
|
|
48
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
48
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-gateway-darwin-arm64"
|
|
49
49
|
},
|
|
50
50
|
"linux-arm64": {
|
|
51
51
|
"file": "tools/linux-arm64/openshell-gateway",
|
|
52
52
|
"sha256": "0e660428e655e7481f34ba8a4b351554400baf791863d32555c98dddb3f09ae2",
|
|
53
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
53
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-gateway-linux-arm64"
|
|
54
54
|
},
|
|
55
55
|
"linux-x64": {
|
|
56
56
|
"file": "tools/linux-x64/openshell-gateway",
|
|
57
57
|
"sha256": "780fc7ad871e2163f3fb866a2d0254ee682fc0d83e6261a84b350c8dcdd9f47b",
|
|
58
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
58
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.8/openshell-gateway-linux-x64"
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -5,6 +5,7 @@ import { companionConfiguration } from "./configuration.js";
|
|
|
5
5
|
import { loadInitialConnections, prepareInitialConnections, readInitialConnectionToken, } from "./connections.js";
|
|
6
6
|
import { configureConnectionsVolume } from "./connections-runtime.js";
|
|
7
7
|
import { writePrivate } from "./state.js";
|
|
8
|
+
import { stageNetworkPolicyChange } from "./network-policy.js";
|
|
8
9
|
import { initialRuntimePolicy } from "./policy.js";
|
|
9
10
|
/** Explicit capability edit, with the installation stopped and its operator lock held. */
|
|
10
11
|
export async function applyConnectionSettings(directory, state, previous, credentialFile) {
|
|
@@ -56,5 +57,7 @@ export async function applyConnectionSettings(directory, state, previous, creden
|
|
|
56
57
|
await writePrivate(file, JSON.stringify(config));
|
|
57
58
|
const rule = initialRuntimePolicy("network_policies: {}", undefined, endpoint)
|
|
58
59
|
.network_policies.connections_broker ?? null;
|
|
59
|
-
await
|
|
60
|
+
await stageNetworkPolicyChange(directory, state, {
|
|
61
|
+
connections_broker: rule,
|
|
62
|
+
});
|
|
60
63
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyNetworkPolicy } from "./network-policy.js";
|
|
2
2
|
import { verifyRuntimeImage } from "./images.js";
|
|
3
3
|
import { startBrowserNode } from "./browser-node-pairing.js";
|
|
4
4
|
import { verifyBrowserNode } from "./browser-node.js";
|
|
@@ -83,7 +83,7 @@ export async function launchLocal(directoryInput, report, control = {}) {
|
|
|
83
83
|
await waitFor(async () => {
|
|
84
84
|
await run(state.input.openshellCli, ["sandbox", "list", "--gateway", name, "--output", "json"], { env, timeout: 5000 });
|
|
85
85
|
});
|
|
86
|
-
await
|
|
86
|
+
await applyNetworkPolicy(directory, state, env);
|
|
87
87
|
if (state.input.modelGateway) {
|
|
88
88
|
report("Starting model gateway…");
|
|
89
89
|
await compose(directory, [
|
|
@@ -113,7 +113,7 @@ export async function launchLocal(directoryInput, report, control = {}) {
|
|
|
113
113
|
await response.body?.cancel();
|
|
114
114
|
});
|
|
115
115
|
await verifyRuntimeBinding(state, runtime);
|
|
116
|
-
await
|
|
116
|
+
await waitFor(() => applyNetworkPolicy(directory, state, env, true));
|
|
117
117
|
if (state.input.browser) {
|
|
118
118
|
report("Starting native browser node…");
|
|
119
119
|
await startBrowserNode(directory, state, cancellation.signal);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { run } from "./process.js";
|
|
6
|
+
import { resourceNames, writePrivate } from "./state.js";
|
|
7
|
+
import { composePublicWebRules } from "./public-web.js";
|
|
8
|
+
import { runtimeManager } from "./runtime.js";
|
|
9
|
+
const connectionRuleSchema = z.object({
|
|
10
|
+
name: z.string(),
|
|
11
|
+
endpoints: z.array(z.object({
|
|
12
|
+
host: z.string(),
|
|
13
|
+
port: z.number(),
|
|
14
|
+
tls: z.string(),
|
|
15
|
+
allowed_ips: z.array(z.string()).optional(),
|
|
16
|
+
})),
|
|
17
|
+
binaries: z.array(z.object({ path: z.string() })),
|
|
18
|
+
});
|
|
19
|
+
const publicRuleSchema = z.object({
|
|
20
|
+
name: z.string(),
|
|
21
|
+
endpoints: z.array(z.object({
|
|
22
|
+
ports: z.array(z.number()),
|
|
23
|
+
allowed_ips: z.array(z.string()),
|
|
24
|
+
tls: z.string(),
|
|
25
|
+
})),
|
|
26
|
+
binaries: z.array(z.object({ path: z.string() })),
|
|
27
|
+
});
|
|
28
|
+
const changesSchema = z.strictObject({
|
|
29
|
+
connections_broker: connectionRuleSchema.nullable().optional(),
|
|
30
|
+
public_web: publicRuleSchema.nullable().optional(),
|
|
31
|
+
});
|
|
32
|
+
export async function stageNetworkPolicyChange(directory, state, rules) {
|
|
33
|
+
const file = join(directory, "private/network-policy-change.json");
|
|
34
|
+
let previous = {};
|
|
35
|
+
try {
|
|
36
|
+
previous = z
|
|
37
|
+
.strictObject({ ownerId: z.literal(state.ownerId), rules: changesSchema })
|
|
38
|
+
.parse(JSON.parse(await readFile(file, "utf8"))).rules;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
await writePrivate(file, JSON.stringify({
|
|
45
|
+
ownerId: state.ownerId,
|
|
46
|
+
rules: changesSchema.parse({ ...previous, ...rules }),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
const policySchema = z.looseObject({
|
|
50
|
+
network_policies: z.record(z.string(), z.unknown()),
|
|
51
|
+
});
|
|
52
|
+
/** Apply only explicitly selected managed rules; never regenerate an operator's other policies. */
|
|
53
|
+
export async function applyNetworkPolicy(directory, state, env, verify = false, command = run) {
|
|
54
|
+
const pending = join(directory, "private/network-policy-change.json");
|
|
55
|
+
let text;
|
|
56
|
+
try {
|
|
57
|
+
text = await readFile(pending, "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
61
|
+
return;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const change = z
|
|
65
|
+
.strictObject({
|
|
66
|
+
ownerId: z.literal(state.ownerId),
|
|
67
|
+
rules: changesSchema,
|
|
68
|
+
})
|
|
69
|
+
.parse(JSON.parse(text));
|
|
70
|
+
const base = join(directory, "private/runtime-policy.json");
|
|
71
|
+
const update = (policy) => {
|
|
72
|
+
return {
|
|
73
|
+
...policy,
|
|
74
|
+
network_policies: composePublicWebRules(Object.fromEntries([
|
|
75
|
+
...Object.entries(policy.network_policies).filter(([key]) => !Object.hasOwn(change.rules, key)),
|
|
76
|
+
...Object.entries(change.rules).filter(([, rule]) => rule !== null),
|
|
77
|
+
])),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const matches = (policy) => {
|
|
81
|
+
const expected = update(policy).network_policies;
|
|
82
|
+
return [
|
|
83
|
+
...new Set([
|
|
84
|
+
...Object.keys(change.rules),
|
|
85
|
+
"connections_broker",
|
|
86
|
+
"model_gateway",
|
|
87
|
+
]),
|
|
88
|
+
].every((key) => {
|
|
89
|
+
const observed = policy.network_policies[key];
|
|
90
|
+
if (!Object.hasOwn(change.rules, key))
|
|
91
|
+
return isDeepStrictEqual(observed, expected[key]);
|
|
92
|
+
if (expected[key] === undefined)
|
|
93
|
+
return observed === undefined;
|
|
94
|
+
const schema = key === "public_web" ? publicRuleSchema : connectionRuleSchema;
|
|
95
|
+
const actualRule = schema.safeParse(observed);
|
|
96
|
+
const expectedRule = schema.safeParse(expected[key]);
|
|
97
|
+
return (actualRule.success &&
|
|
98
|
+
expectedRule.success &&
|
|
99
|
+
isDeepStrictEqual(actualRule.data, expectedRule.data));
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const name = resourceNames(state).sandbox;
|
|
103
|
+
if (!(await runtimeManager(directory, state, env, command).recorded()).receipt) {
|
|
104
|
+
if (verify)
|
|
105
|
+
throw Error("The configured runtime has not been recorded.");
|
|
106
|
+
await writePrivate(base, JSON.stringify(update(policySchema.parse(JSON.parse(await readFile(base, "utf8"))))));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const observed = z
|
|
110
|
+
.object({
|
|
111
|
+
sandbox: z.literal(name),
|
|
112
|
+
policy_source: z.literal("sandbox"),
|
|
113
|
+
version: z.number().int(),
|
|
114
|
+
active_version: z.number().int(),
|
|
115
|
+
status: z.string(),
|
|
116
|
+
policy: policySchema,
|
|
117
|
+
})
|
|
118
|
+
.parse(JSON.parse(await command(state.input.openshellCli, [
|
|
119
|
+
"policy",
|
|
120
|
+
"get",
|
|
121
|
+
name,
|
|
122
|
+
"--gateway",
|
|
123
|
+
name,
|
|
124
|
+
"--base",
|
|
125
|
+
"--output",
|
|
126
|
+
"json",
|
|
127
|
+
], { env })));
|
|
128
|
+
if (matches(observed.policy)) {
|
|
129
|
+
if (verify) {
|
|
130
|
+
if (observed.active_version !== observed.version ||
|
|
131
|
+
observed.status !== "effective")
|
|
132
|
+
throw Error("The selected network policy has not become effective in the runtime.");
|
|
133
|
+
await rm(pending);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (verify)
|
|
138
|
+
throw Error("The selected network policy was not confirmed. The installation remains unavailable.");
|
|
139
|
+
await writePrivate(base, JSON.stringify(update(observed.policy)));
|
|
140
|
+
await command(state.input.openshellCli, ["policy", "set", name, "--gateway", name, "--policy", base], { env });
|
|
141
|
+
}
|
|
@@ -4,7 +4,8 @@ import { parseDocument } from "yaml";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { LocalSetupError } from "./process.js";
|
|
6
6
|
import { ensurePrivateFile } from "./state.js";
|
|
7
|
-
|
|
7
|
+
import { publicWebPolicy, publicWebEndpointConstraints } from "./public-web.js";
|
|
8
|
+
export function initialRuntimePolicy(source, models, connections, publicWeb = false) {
|
|
8
9
|
const document = parseDocument(source);
|
|
9
10
|
if (document.errors.length || document.warnings.length)
|
|
10
11
|
throw new LocalSetupError("invalid_runtime_policy", "The shipped runtime policy is invalid.");
|
|
@@ -17,6 +18,7 @@ export function initialRuntimePolicy(source, models, connections) {
|
|
|
17
18
|
return {
|
|
18
19
|
...parsed.data,
|
|
19
20
|
network_policies: {
|
|
21
|
+
...(publicWeb ? { public_web: publicWebPolicy(true) } : {}),
|
|
20
22
|
...(connections
|
|
21
23
|
? {
|
|
22
24
|
connections_broker: {
|
|
@@ -25,6 +27,7 @@ export function initialRuntimePolicy(source, models, connections) {
|
|
|
25
27
|
{
|
|
26
28
|
host: connections.network.host,
|
|
27
29
|
port: connections.network.port,
|
|
30
|
+
...publicWebEndpointConstraints(connections.network.port, publicWeb),
|
|
28
31
|
tls: "skip",
|
|
29
32
|
},
|
|
30
33
|
],
|
|
@@ -40,6 +43,7 @@ export function initialRuntimePolicy(source, models, connections) {
|
|
|
40
43
|
{
|
|
41
44
|
host: models.network.host,
|
|
42
45
|
port: models.network.port,
|
|
46
|
+
...publicWebEndpointConstraints(models.network.port, publicWeb),
|
|
43
47
|
tls: "skip",
|
|
44
48
|
},
|
|
45
49
|
],
|
|
@@ -50,7 +54,7 @@ export function initialRuntimePolicy(source, models, connections) {
|
|
|
50
54
|
},
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
|
-
export async function prepareRuntimePolicy(directory, models, connections) {
|
|
54
|
-
const policy = initialRuntimePolicy(await readFile(new URL("../../deploy/openshell/policy.yaml", import.meta.url), "utf8"), models, connections);
|
|
57
|
+
export async function prepareRuntimePolicy(directory, models, connections, publicWeb = false) {
|
|
58
|
+
const policy = initialRuntimePolicy(await readFile(new URL("../../deploy/openshell/policy.yaml", import.meta.url), "utf8"), models, connections, publicWeb);
|
|
55
59
|
await ensurePrivateFile(join(directory, "private/runtime-policy.json"), JSON.stringify(policy));
|
|
56
60
|
}
|
|
@@ -93,7 +93,7 @@ export async function prepareLocal(directoryInput, inputValue, capabilities) {
|
|
|
93
93
|
: 0), null, 2));
|
|
94
94
|
await prepareModelCredential(directory, state);
|
|
95
95
|
const models = await prepareInitialModels(directory, input.models);
|
|
96
|
-
await prepareRuntimePolicy(directory, models, connectionsEndpoint);
|
|
96
|
+
await prepareRuntimePolicy(directory, models, connectionsEndpoint, state.input.publicWeb);
|
|
97
97
|
await withPreparedDatabase(directory, state, async (pool, runtimeUrl) => {
|
|
98
98
|
const store = new PostgresAccessStore(pool, await readFile(join(privateDirectory, "encryption.key")), {
|
|
99
99
|
issuer: input.team.issuer,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { LocalSetupError } from "./process.js";
|
|
4
|
+
// Public unicast ranges, excluding private and special-use destinations.
|
|
5
|
+
// Keep aligned with the browser egress boundary in deploy/execution/network/squid.conf.
|
|
6
|
+
export const publicWebAddresses = [
|
|
7
|
+
"1.0.0.0/8",
|
|
8
|
+
"2.0.0.0/7",
|
|
9
|
+
"4.0.0.0/6",
|
|
10
|
+
"8.0.0.0/7",
|
|
11
|
+
"11.0.0.0/8",
|
|
12
|
+
"12.0.0.0/6",
|
|
13
|
+
"16.0.0.0/4",
|
|
14
|
+
"32.0.0.0/3",
|
|
15
|
+
"64.0.0.0/3",
|
|
16
|
+
"96.0.0.0/6",
|
|
17
|
+
"100.0.0.0/10",
|
|
18
|
+
"100.128.0.0/9",
|
|
19
|
+
"101.0.0.0/8",
|
|
20
|
+
"102.0.0.0/7",
|
|
21
|
+
"104.0.0.0/5",
|
|
22
|
+
"112.0.0.0/5",
|
|
23
|
+
"120.0.0.0/6",
|
|
24
|
+
"124.0.0.0/7",
|
|
25
|
+
"126.0.0.0/8",
|
|
26
|
+
"128.0.0.0/3",
|
|
27
|
+
"160.0.0.0/5",
|
|
28
|
+
"168.0.0.0/8",
|
|
29
|
+
"169.0.0.0/9",
|
|
30
|
+
"169.128.0.0/10",
|
|
31
|
+
"169.192.0.0/11",
|
|
32
|
+
"169.224.0.0/12",
|
|
33
|
+
"169.240.0.0/13",
|
|
34
|
+
"169.248.0.0/14",
|
|
35
|
+
"169.252.0.0/15",
|
|
36
|
+
"169.255.0.0/16",
|
|
37
|
+
"170.0.0.0/7",
|
|
38
|
+
"172.0.0.0/12",
|
|
39
|
+
"172.32.0.0/11",
|
|
40
|
+
"172.64.0.0/10",
|
|
41
|
+
"172.128.0.0/9",
|
|
42
|
+
"173.0.0.0/8",
|
|
43
|
+
"174.0.0.0/7",
|
|
44
|
+
"176.0.0.0/4",
|
|
45
|
+
"192.0.1.0/24",
|
|
46
|
+
"192.0.3.0/24",
|
|
47
|
+
"192.0.4.0/22",
|
|
48
|
+
"192.0.8.0/21",
|
|
49
|
+
"192.0.16.0/20",
|
|
50
|
+
"192.0.32.0/19",
|
|
51
|
+
"192.0.64.0/18",
|
|
52
|
+
"192.0.128.0/17",
|
|
53
|
+
"192.1.0.0/16",
|
|
54
|
+
"192.2.0.0/15",
|
|
55
|
+
"192.4.0.0/14",
|
|
56
|
+
"192.8.0.0/13",
|
|
57
|
+
"192.16.0.0/12",
|
|
58
|
+
"192.32.0.0/11",
|
|
59
|
+
"192.64.0.0/12",
|
|
60
|
+
"192.80.0.0/13",
|
|
61
|
+
"192.88.0.0/18",
|
|
62
|
+
"192.88.64.0/19",
|
|
63
|
+
"192.88.96.0/23",
|
|
64
|
+
"192.88.98.0/24",
|
|
65
|
+
"192.88.100.0/22",
|
|
66
|
+
"192.88.104.0/21",
|
|
67
|
+
"192.88.112.0/20",
|
|
68
|
+
"192.88.128.0/17",
|
|
69
|
+
"192.89.0.0/16",
|
|
70
|
+
"192.90.0.0/15",
|
|
71
|
+
"192.92.0.0/14",
|
|
72
|
+
"192.96.0.0/11",
|
|
73
|
+
"192.128.0.0/11",
|
|
74
|
+
"192.160.0.0/13",
|
|
75
|
+
"192.169.0.0/16",
|
|
76
|
+
"192.170.0.0/15",
|
|
77
|
+
"192.172.0.0/14",
|
|
78
|
+
"192.176.0.0/12",
|
|
79
|
+
"192.192.0.0/10",
|
|
80
|
+
"193.0.0.0/8",
|
|
81
|
+
"194.0.0.0/7",
|
|
82
|
+
"196.0.0.0/7",
|
|
83
|
+
"198.0.0.0/12",
|
|
84
|
+
"198.16.0.0/15",
|
|
85
|
+
"198.20.0.0/14",
|
|
86
|
+
"198.24.0.0/13",
|
|
87
|
+
"198.32.0.0/12",
|
|
88
|
+
"198.48.0.0/15",
|
|
89
|
+
"198.50.0.0/16",
|
|
90
|
+
"198.51.0.0/18",
|
|
91
|
+
"198.51.64.0/19",
|
|
92
|
+
"198.51.96.0/22",
|
|
93
|
+
"198.51.101.0/24",
|
|
94
|
+
"198.51.102.0/23",
|
|
95
|
+
"198.51.104.0/21",
|
|
96
|
+
"198.51.112.0/20",
|
|
97
|
+
"198.51.128.0/17",
|
|
98
|
+
"198.52.0.0/14",
|
|
99
|
+
"198.56.0.0/13",
|
|
100
|
+
"198.64.0.0/10",
|
|
101
|
+
"198.128.0.0/9",
|
|
102
|
+
"199.0.0.0/8",
|
|
103
|
+
"200.0.0.0/7",
|
|
104
|
+
"202.0.0.0/8",
|
|
105
|
+
"203.0.0.0/18",
|
|
106
|
+
"203.0.64.0/19",
|
|
107
|
+
"203.0.96.0/20",
|
|
108
|
+
"203.0.112.0/24",
|
|
109
|
+
"203.0.114.0/23",
|
|
110
|
+
"203.0.116.0/22",
|
|
111
|
+
"203.0.120.0/21",
|
|
112
|
+
"203.0.128.0/17",
|
|
113
|
+
"203.1.0.0/16",
|
|
114
|
+
"203.2.0.0/15",
|
|
115
|
+
"203.4.0.0/14",
|
|
116
|
+
"203.8.0.0/13",
|
|
117
|
+
"203.16.0.0/12",
|
|
118
|
+
"203.32.0.0/11",
|
|
119
|
+
"203.64.0.0/10",
|
|
120
|
+
"203.128.0.0/9",
|
|
121
|
+
"204.0.0.0/6",
|
|
122
|
+
"208.0.0.0/4",
|
|
123
|
+
"2000::/16",
|
|
124
|
+
"2001:200::/23",
|
|
125
|
+
"2001:400::/22",
|
|
126
|
+
"2001:800::/22",
|
|
127
|
+
"2001:c00::/24",
|
|
128
|
+
"2001:d00::/25",
|
|
129
|
+
"2001:d80::/27",
|
|
130
|
+
"2001:da0::/28",
|
|
131
|
+
"2001:db0::/29",
|
|
132
|
+
"2001:db9::/32",
|
|
133
|
+
"2001:dba::/31",
|
|
134
|
+
"2001:dbc::/30",
|
|
135
|
+
"2001:dc0::/26",
|
|
136
|
+
"2001:e00::/23",
|
|
137
|
+
"2001:1000::/20",
|
|
138
|
+
"2001:2000::/19",
|
|
139
|
+
"2001:4000::/18",
|
|
140
|
+
"2001:8000::/17",
|
|
141
|
+
"2003::/16",
|
|
142
|
+
"2004::/14",
|
|
143
|
+
"2008::/13",
|
|
144
|
+
"2010::/12",
|
|
145
|
+
"2020::/11",
|
|
146
|
+
"2040::/10",
|
|
147
|
+
"2080::/9",
|
|
148
|
+
"2100::/8",
|
|
149
|
+
"2200::/7",
|
|
150
|
+
"2400::/6",
|
|
151
|
+
"2800::/5",
|
|
152
|
+
"3000::/5",
|
|
153
|
+
"3800::/6",
|
|
154
|
+
"3c00::/7",
|
|
155
|
+
"3e00::/8",
|
|
156
|
+
"3f00::/9",
|
|
157
|
+
"3f80::/10",
|
|
158
|
+
"3fc0::/11",
|
|
159
|
+
"3fe0::/12",
|
|
160
|
+
"3ff0::/13",
|
|
161
|
+
"3ff8::/14",
|
|
162
|
+
"3ffc::/15",
|
|
163
|
+
"3ffe::/16",
|
|
164
|
+
"3fff:1000::/20",
|
|
165
|
+
"3fff:2000::/19",
|
|
166
|
+
"3fff:4000::/18",
|
|
167
|
+
"3fff:8000::/17",
|
|
168
|
+
];
|
|
169
|
+
export function publicWebPolicy(enabled) {
|
|
170
|
+
return enabled
|
|
171
|
+
? {
|
|
172
|
+
name: "Public web",
|
|
173
|
+
endpoints: [
|
|
174
|
+
{ ports: [80, 443], allowed_ips: publicWebAddresses, tls: "skip" },
|
|
175
|
+
],
|
|
176
|
+
binaries: [{ path: "/**" }],
|
|
177
|
+
}
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
180
|
+
/** OpenShell requires overlapping endpoints to have identical IP constraints. */
|
|
181
|
+
export function publicWebEndpointConstraints(port, enabled) {
|
|
182
|
+
return enabled && (port === 80 || port === 443)
|
|
183
|
+
? { allowed_ips: publicWebAddresses }
|
|
184
|
+
: {};
|
|
185
|
+
}
|
|
186
|
+
const managedRuleSchema = z.looseObject({
|
|
187
|
+
endpoints: z.array(z.looseObject({
|
|
188
|
+
port: z.number(),
|
|
189
|
+
allowed_ips: z.array(z.string()).optional(),
|
|
190
|
+
})),
|
|
191
|
+
});
|
|
192
|
+
/** Compose managed service rules with public web without broadening private access. */
|
|
193
|
+
export function composePublicWebRules(rules) {
|
|
194
|
+
const enabled = rules.public_web !== undefined;
|
|
195
|
+
const result = { ...rules };
|
|
196
|
+
for (const key of ["connections_broker", "model_gateway"]) {
|
|
197
|
+
const rule = result[key];
|
|
198
|
+
if (rule === undefined)
|
|
199
|
+
continue;
|
|
200
|
+
const parsed = managedRuleSchema.safeParse(rule);
|
|
201
|
+
if (!parsed.success)
|
|
202
|
+
throw new LocalSetupError("invalid_runtime_policy", "A managed service network rule has been edited into an unsupported shape. Review its policy before changing public web access.");
|
|
203
|
+
result[key] = {
|
|
204
|
+
...parsed.data,
|
|
205
|
+
endpoints: parsed.data.endpoints.map((endpoint) => {
|
|
206
|
+
if (endpoint.port !== 80 && endpoint.port !== 443)
|
|
207
|
+
return endpoint;
|
|
208
|
+
const managed = isDeepStrictEqual(endpoint.allowed_ips, publicWebAddresses);
|
|
209
|
+
if (enabled && endpoint.allowed_ips?.length && !managed)
|
|
210
|
+
throw new LocalSetupError("invalid_runtime_policy", "Public web overlaps a managed service's custom IP restrictions on port 80 or 443. Keep public web off or reconcile those restrictions explicitly.");
|
|
211
|
+
if (!enabled && !managed)
|
|
212
|
+
return endpoint;
|
|
213
|
+
const { allowed_ips: _previous, ...rest } = endpoint;
|
|
214
|
+
return {
|
|
215
|
+
...rest,
|
|
216
|
+
...publicWebEndpointConstraints(endpoint.port, enabled),
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
@@ -94,7 +94,7 @@ export function installationCommand(observation) {
|
|
|
94
94
|
});
|
|
95
95
|
const directory = await resolveLocation(options);
|
|
96
96
|
const result = await progress("Deleting installation", (signal) => deleteInstallation(directory, signal), program.opts());
|
|
97
|
-
output(result, "Installation deleted from Docker.
|
|
97
|
+
output(result, "Installation deleted from Docker. The local folder still contains configuration and credentials. To install again, move or remove that folder, or configure a new --directory.");
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
if (options.confirmDelete !== undefined || options.acceptDataLoss)
|
|
@@ -65,6 +65,7 @@ export const installationSchema = z
|
|
|
65
65
|
}),
|
|
66
66
|
]),
|
|
67
67
|
resources: z.strictObject({ runtime: resources }),
|
|
68
|
+
publicWeb: z.boolean().default(false),
|
|
68
69
|
browser: z.strictObject({ enabled: z.boolean() }),
|
|
69
70
|
models: z.discriminatedUnion("mode", [
|
|
70
71
|
externalLiteLlmSchema,
|
|
@@ -117,6 +117,7 @@ export async function configurationChanges(before, after) {
|
|
|
117
117
|
return {
|
|
118
118
|
models: !isDeepStrictEqual(await models(before), await models(after)),
|
|
119
119
|
connections: !isDeepStrictEqual(before.connections, after.connections),
|
|
120
|
+
publicWeb: before.publicWeb !== after.publicWeb,
|
|
120
121
|
packs: !isDeepStrictEqual(await packs(before), await packs(after)),
|
|
121
122
|
};
|
|
122
123
|
}
|
|
@@ -175,6 +175,21 @@ export async function collectInstallation(ui, options, retained) {
|
|
|
175
175
|
case "resources":
|
|
176
176
|
config.resources = await collectResources(ui, config.resources);
|
|
177
177
|
break;
|
|
178
|
+
case "public-web":
|
|
179
|
+
config.publicWeb =
|
|
180
|
+
(await ui.select("Public web access", [
|
|
181
|
+
{
|
|
182
|
+
value: "on",
|
|
183
|
+
label: "On",
|
|
184
|
+
hint: "Public HTTP(S); agents can send team data to public services",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
value: "off",
|
|
188
|
+
label: "Off",
|
|
189
|
+
hint: "Only configured services and explicit policies",
|
|
190
|
+
},
|
|
191
|
+
], config.publicWeb ? "on" : "off")) === "on";
|
|
192
|
+
break;
|
|
178
193
|
case "browser":
|
|
179
194
|
ui.note("Explicit browser node use works. Ordinary model-selected browsing is not qualified because of an upstream routing issue.", "Experimental browser");
|
|
180
195
|
config.browser.enabled =
|
|
@@ -25,6 +25,7 @@ export function installationMenu(config, directory, browserAvailable, packIssues
|
|
|
25
25
|
row("exposure", "Network", config.exposure.mode === "local"
|
|
26
26
|
? "This computer"
|
|
27
27
|
: config.exposure.applicationOrigin),
|
|
28
|
+
row("public-web", "Public web", config.publicWeb ? "On" : "Off", "HTTP(S) for agents and tools; private destinations blocked"),
|
|
28
29
|
row("models", "Models", modelSummary ?? "Choose a model"),
|
|
29
30
|
row("connections", "Connections", config.connections.mode === "disabled" ? "Off" : "On"),
|
|
30
31
|
row("packs", "Packs", packIssues.length
|
|
@@ -42,7 +43,7 @@ export function installationMenu(config, directory, browserAvailable, packIssues
|
|
|
42
43
|
];
|
|
43
44
|
return existing
|
|
44
45
|
? [
|
|
45
|
-
...choices.filter(({ value }) => ["review", "models", "connections", "packs"].includes(value)),
|
|
46
|
+
...choices.filter(({ value }) => ["review", "models", "connections", "packs", "public-web"].includes(value)),
|
|
46
47
|
{ value: "model-credentials", label: "Change LLM API keys" },
|
|
47
48
|
]
|
|
48
49
|
: choices;
|
|
@@ -95,7 +95,7 @@ export async function progress(message, work, options = {}) {
|
|
|
95
95
|
const report = (detail) => {
|
|
96
96
|
if (spinner && detail.includes("\n")) {
|
|
97
97
|
spinner.stop(message);
|
|
98
|
-
|
|
98
|
+
clack.log.info(detail, { output: process.stderr });
|
|
99
99
|
spinner.start(message);
|
|
100
100
|
}
|
|
101
101
|
else if (spinner)
|
|
@@ -193,6 +193,18 @@ export async function runConfiguration(options, observeMode) {
|
|
|
193
193
|
throw error;
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
if (existing && state) {
|
|
197
|
+
try {
|
|
198
|
+
await access(join(state, "prepared.json"));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (!(error instanceof Error &&
|
|
202
|
+
"code" in error &&
|
|
203
|
+
error.code === "ENOENT"))
|
|
204
|
+
throw error;
|
|
205
|
+
throw new InstallationError("invalid_configuration", `This folder retains settings but the installation is no longer prepared. If deletion failed, finish it with clawscarf stop --directory ${quote(options.directory ?? defaultInstallationDirectory)} --delete. After successful deletion, move or remove the installation folder before configuring it again, or choose a new --directory.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
196
208
|
observeMode?.(existing ? "edit" : "new");
|
|
197
209
|
let result = existing && state
|
|
198
210
|
? await editInstallationSettings(state, ui, options)
|
|
@@ -219,14 +231,17 @@ export async function runConfiguration(options, observeMode) {
|
|
|
219
231
|
: undefined;
|
|
220
232
|
if (result.state === "cancelled")
|
|
221
233
|
clack.cancel("Cancelled.");
|
|
222
|
-
else
|
|
234
|
+
else {
|
|
235
|
+
if ("ready" in result && result.ready)
|
|
236
|
+
ui.note(`clawscarf status --directory ${quote(options.directory ?? defaultInstallationDirectory)}\nclawscarf stop --directory ${quote(options.directory ?? defaultInstallationDirectory)}`, "Manage this installation");
|
|
223
237
|
clack.outro("ready" in result
|
|
224
238
|
? result.ready
|
|
225
|
-
? `ClawScarf is ready.${applicationUrl ? ` Visit ${terminalLink(applicationUrl)}` : ""}
|
|
239
|
+
? `ClawScarf is ready.${applicationUrl ? ` Visit ${terminalLink(applicationUrl)}` : ""}`
|
|
226
240
|
: "ClawScarf needs attention. Check status for details."
|
|
227
241
|
: result.state === "unchanged"
|
|
228
242
|
? "No changes. Server left as it was."
|
|
229
243
|
: "Configuration saved. Server stopped.");
|
|
244
|
+
}
|
|
230
245
|
if ("ready" in result && result.ready && applicationUrl)
|
|
231
246
|
await ui.openBrowser(applicationUrl);
|
|
232
247
|
}
|
|
@@ -21,7 +21,7 @@ import { withInstallationLock, writePrivate } from "../../deployment/state.js";
|
|
|
21
21
|
export async function editInstallationSettings(directory, ui, options = {}, prerequisites = { host: checkHost, check: checkInstallationPrerequisites }) {
|
|
22
22
|
directory = resolve(directory);
|
|
23
23
|
if (options.recipe || options.cloudUrl)
|
|
24
|
-
throw new InstallationError("change_unsupported", "Existing installations keep their recipe, software release and login service. Change models, keys, Connections or packs instead.");
|
|
24
|
+
throw new InstallationError("change_unsupported", "Existing installations keep their recipe, software release and login service. Change models, keys, Connections, public web or packs instead.");
|
|
25
25
|
if (options.nonInteractive && !options.yes)
|
|
26
26
|
throw new InstallationError("invalid_configuration", "Changing an existing installation noninteractively requires --yes. The server may restart and deselected packs may be removed.");
|
|
27
27
|
const task = (message, work) => progress(message, work, options);
|
|
@@ -114,6 +114,9 @@ export async function editInstallationSettings(directory, ui, options = {}, prer
|
|
|
114
114
|
`Connections: ${plan.changes.connections.to === "disabled" ? "Off" : "On"}`,
|
|
115
115
|
]
|
|
116
116
|
: []),
|
|
117
|
+
...(plan.scopes.publicWeb
|
|
118
|
+
? [`Public web: ${plan.changes.publicWeb.to ? "On" : "Off"}`]
|
|
119
|
+
: []),
|
|
117
120
|
...(plan.scopes.packs
|
|
118
121
|
? [`Packs: ${plan.changes.packs.selected.join(", ") || "None"}`]
|
|
119
122
|
: []),
|
|
@@ -39,6 +39,7 @@ export async function installationSummary(config, inputs) {
|
|
|
39
39
|
`Administrator: ${config.access.administratorName} · ${config.access.mode === "hosted" ? "ClawScarf login" : "Custom OIDC"}`,
|
|
40
40
|
`Models: ${model}`,
|
|
41
41
|
`Connections: ${config.connections.mode === "disabled" ? "Off" : "On"}`,
|
|
42
|
+
`Public web: ${config.publicWeb ? "On" : "Off"} (private destinations blocked)`,
|
|
42
43
|
`Browser: ${config.browser.enabled ? "Experimental" : "Off"} · Packs: ${config.packs.flatMap((pack) => pack.members).join(", ") || "None"}`,
|
|
43
44
|
].join("\n");
|
|
44
45
|
}
|
|
@@ -36,6 +36,7 @@ export const selectionSchema = z.object({
|
|
|
36
36
|
connections: z.boolean().optional(),
|
|
37
37
|
connectionsCloudUrl: cloudUrlSchema.optional(),
|
|
38
38
|
browser: z.boolean().optional(),
|
|
39
|
+
publicWeb: z.boolean().optional(),
|
|
39
40
|
model: text.optional(),
|
|
40
41
|
provider: text.optional(),
|
|
41
42
|
reasoning: z.enum(["low", "medium", "high"]).optional(),
|
|
@@ -79,6 +80,8 @@ export function installationOptions(command) {
|
|
|
79
80
|
.option("--connections-cloud-url <url>", "Use a separate Connections service")
|
|
80
81
|
.option("--browser", "Enable experimental browser capability")
|
|
81
82
|
.option("--no-browser", "Disable browser capability")
|
|
83
|
+
.option("--public-web", "Allow public HTTP(S) from agents and tools")
|
|
84
|
+
.option("--no-public-web", "Restrict runtime egress to configured services")
|
|
82
85
|
.option("--model <id>", "Default model from the model catalog")
|
|
83
86
|
.option("--provider <id>", "Model provider, for example openai or openrouter")
|
|
84
87
|
.addOption(new Option("--reasoning <level>", "Model reasoning level").choices([
|
|
@@ -116,6 +119,7 @@ export async function selectedDraft(context, recipe, selections, inputs, retaine
|
|
|
116
119
|
"modelGatewayKeyFile",
|
|
117
120
|
"modelGatewayCaFile",
|
|
118
121
|
"connections",
|
|
122
|
+
"publicWeb",
|
|
119
123
|
"connectionsCloudUrl",
|
|
120
124
|
"pack",
|
|
121
125
|
"packs",
|
|
@@ -123,7 +127,7 @@ export async function selectedDraft(context, recipe, selections, inputs, retaine
|
|
|
123
127
|
"packPython",
|
|
124
128
|
]);
|
|
125
129
|
if (Object.entries(o).some(([key, value]) => value !== undefined && !mutable.has(key)))
|
|
126
|
-
invalid("Existing installations support changes to models, keys, Connections and packs only.");
|
|
130
|
+
invalid("Existing installations support changes to models, keys, Connections, public web and packs only.");
|
|
127
131
|
}
|
|
128
132
|
const config = structuredClone(retained ?? recipeConfiguration(context, recipe));
|
|
129
133
|
if (o.name !== undefined)
|
|
@@ -136,6 +140,8 @@ export async function selectedDraft(context, recipe, selections, inputs, retaine
|
|
|
136
140
|
config.resources.runtime.memory = o.memory;
|
|
137
141
|
if (o.browser !== undefined)
|
|
138
142
|
config.browser.enabled = o.browser;
|
|
143
|
+
if (o.publicWeb !== undefined)
|
|
144
|
+
config.publicWeb = o.publicWeb;
|
|
139
145
|
if (o.connections !== undefined)
|
|
140
146
|
config.connections.mode = o.connections ? "hosted" : "disabled";
|
|
141
147
|
if (o.connectionsCloudUrl)
|
|
@@ -11,6 +11,7 @@ export const recipeSchema = z.strictObject({
|
|
|
11
11
|
defaults: z.strictObject({
|
|
12
12
|
resources: installationSchema.shape.resources,
|
|
13
13
|
browser: installationSchema.shape.browser,
|
|
14
|
+
publicWeb: z.boolean().default(false),
|
|
14
15
|
connections: z.strictObject({ enabled: z.boolean() }).optional(),
|
|
15
16
|
}),
|
|
16
17
|
models: z.strictObject({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { publicWebPolicy } from "../deployment/public-web.js";
|
|
2
|
+
import { stageNetworkPolicyChange } from "../deployment/network-policy.js";
|
|
1
3
|
import { applyConnectionSettings } from "../deployment/connection-settings.js";
|
|
2
4
|
import { readFile } from "node:fs/promises";
|
|
3
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -41,8 +43,8 @@ export async function planSettingsChange(configFile, reapply) {
|
|
|
41
43
|
const previousPacks = selectionSchema.parse(await readJson(join(directory, "packs.json")));
|
|
42
44
|
if (!desired.packSelection.python && previousPacks.python)
|
|
43
45
|
desired.packSelection.python = previousPacks.python;
|
|
44
|
-
const { models: _oldModels, modelGateway: oldGateway, connections: oldConnections, ...oldFixed } = old;
|
|
45
|
-
const { models: _nextModels, modelGateway: nextGateway, connections: nextConnections, ...nextFixed } = next;
|
|
46
|
+
const { models: _oldModels, modelGateway: oldGateway, connections: oldConnections, publicWeb: oldPublicWeb, ...oldFixed } = old;
|
|
47
|
+
const { models: _nextModels, modelGateway: nextGateway, connections: nextConnections, publicWeb: nextPublicWeb, ...nextFixed } = next;
|
|
46
48
|
if (!isDeepStrictEqual(oldFixed, nextFixed) ||
|
|
47
49
|
!isDeepStrictEqual(await readJson(join(directory, "release.json")), desired.release))
|
|
48
50
|
unsupported("Settings changes cannot replace the release, identity, addresses, access, resources or execution protection.");
|
|
@@ -92,6 +94,7 @@ export async function planSettingsChange(configFile, reapply) {
|
|
|
92
94
|
scopes,
|
|
93
95
|
reapply,
|
|
94
96
|
changes: {
|
|
97
|
+
publicWeb: { from: oldPublicWeb, to: nextPublicWeb },
|
|
95
98
|
models: modelConfiguration
|
|
96
99
|
? {
|
|
97
100
|
enabled: modelConfiguration.models
|
|
@@ -235,6 +238,11 @@ export async function reconfigureInstallation(configFile, expectedFingerprint, r
|
|
|
235
238
|
if (scopes.connections &&
|
|
236
239
|
(state.input.connections || desired.input.connections))
|
|
237
240
|
await applyConnectionSettings(directory, { ...state, input: desired.input }, checked.previousEndpoint, desired.connectorCredentialFile);
|
|
241
|
+
stage = "public web policy";
|
|
242
|
+
if (scopes.publicWeb)
|
|
243
|
+
await stageNetworkPolicyChange(directory, state, {
|
|
244
|
+
public_web: publicWebPolicy(desired.input.publicWeb),
|
|
245
|
+
});
|
|
238
246
|
stage = "accepted settings";
|
|
239
247
|
if (scopes.models)
|
|
240
248
|
await writePrivate(join(directory, "private/model-bootstrap.json"), JSON.stringify(models));
|
|
@@ -73,6 +73,7 @@ export async function resolveInstallation(configFile, internalPorts) {
|
|
|
73
73
|
name: config.name,
|
|
74
74
|
administratorName: access.administratorName,
|
|
75
75
|
runtimeImage: release.images.gateway,
|
|
76
|
+
publicWeb: config.publicWeb,
|
|
76
77
|
companionImage: release.images.companion,
|
|
77
78
|
...(config.browser.enabled ? { relayImage: release.images.relay } : {}),
|
|
78
79
|
openshellCli: cli,
|
|
@@ -57,6 +57,7 @@ export function recipeConfiguration(context, recipeId) {
|
|
|
57
57
|
},
|
|
58
58
|
resources: structuredClone(recipe.defaults.resources),
|
|
59
59
|
browser: structuredClone(recipe.defaults.browser),
|
|
60
|
+
publicWeb: recipe.defaults.publicWeb,
|
|
60
61
|
connections: {
|
|
61
62
|
mode: recipe.defaults.connections?.enabled ? "hosted" : "disabled",
|
|
62
63
|
cloudUrl: context.cloudUrl,
|
|
@@ -55,6 +55,15 @@ export const releaseSchema = z.strictObject({
|
|
|
55
55
|
}),
|
|
56
56
|
}),
|
|
57
57
|
});
|
|
58
|
+
/** Published manifests must work without checkout-local images or executables. */
|
|
59
|
+
export function assertPublishedRuntime(runtime) {
|
|
60
|
+
if (JSON.stringify(runtime.images).includes('"sha256:') ||
|
|
61
|
+
runtime.platforms.some((platform) => {
|
|
62
|
+
const tools = releaseTools(runtime, platform);
|
|
63
|
+
return !tools.cli.url || !tools.gateway.url;
|
|
64
|
+
}))
|
|
65
|
+
throw Error("Published runtimes require registry digests and downloadable tools.");
|
|
66
|
+
}
|
|
58
67
|
/** Single-platform development bundles and multi-platform published bundles share resolution. */
|
|
59
68
|
export function releaseTools(release, platform = `${process.platform}-${process.arch}`) {
|
|
60
69
|
const host = hostPlatformSchema.parse(platform);
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { run } from "./process.js";
|
|
6
|
-
import { resourceNames, writePrivate } from "./state.js";
|
|
7
|
-
import { runtimeManager } from "./runtime.js";
|
|
8
|
-
const ruleSchema = z.object({
|
|
9
|
-
name: z.string(),
|
|
10
|
-
endpoints: z.array(z.object({ host: z.string(), port: z.number(), tls: z.string() })),
|
|
11
|
-
binaries: z.array(z.object({ path: z.string() })),
|
|
12
|
-
});
|
|
13
|
-
const policySchema = z.looseObject({
|
|
14
|
-
network_policies: z.record(z.string(), z.unknown()),
|
|
15
|
-
});
|
|
16
|
-
/** Apply only the explicitly selected broker rule; never regenerate an operator's other policies. */
|
|
17
|
-
export async function applyConnectionPolicy(directory, state, env, verify = false) {
|
|
18
|
-
const pending = join(directory, "private/connection-policy-change.json");
|
|
19
|
-
let text;
|
|
20
|
-
try {
|
|
21
|
-
text = await readFile(pending, "utf8");
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
25
|
-
return;
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
const change = z
|
|
29
|
-
.strictObject({
|
|
30
|
-
ownerId: z.literal(state.ownerId),
|
|
31
|
-
rule: ruleSchema.nullable(),
|
|
32
|
-
})
|
|
33
|
-
.parse(JSON.parse(text));
|
|
34
|
-
const base = join(directory, "private/runtime-policy.json");
|
|
35
|
-
const update = (policy) => {
|
|
36
|
-
if (change.rule)
|
|
37
|
-
policy.network_policies.connections_broker = change.rule;
|
|
38
|
-
else
|
|
39
|
-
delete policy.network_policies.connections_broker;
|
|
40
|
-
return policy;
|
|
41
|
-
};
|
|
42
|
-
const matches = (rule) => {
|
|
43
|
-
if (change.rule === null)
|
|
44
|
-
return rule === undefined;
|
|
45
|
-
const parsed = ruleSchema.safeParse(rule);
|
|
46
|
-
return parsed.success && isDeepStrictEqual(parsed.data, change.rule);
|
|
47
|
-
};
|
|
48
|
-
const name = resourceNames(state).sandbox;
|
|
49
|
-
if (!(await runtimeManager(directory, state, env, run).recorded()).receipt) {
|
|
50
|
-
if (verify)
|
|
51
|
-
throw Error("The configured runtime has not been recorded.");
|
|
52
|
-
await writePrivate(base, JSON.stringify(update(policySchema.parse(JSON.parse(await readFile(base, "utf8"))))));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const observed = z
|
|
56
|
-
.object({
|
|
57
|
-
sandbox: z.literal(name),
|
|
58
|
-
policy_source: z.literal("sandbox"),
|
|
59
|
-
policy: policySchema,
|
|
60
|
-
})
|
|
61
|
-
.parse(JSON.parse(await run(state.input.openshellCli, [
|
|
62
|
-
"policy",
|
|
63
|
-
"get",
|
|
64
|
-
name,
|
|
65
|
-
"--gateway",
|
|
66
|
-
name,
|
|
67
|
-
"--base",
|
|
68
|
-
"--output",
|
|
69
|
-
"json",
|
|
70
|
-
], { env })));
|
|
71
|
-
if (matches(observed.policy.network_policies.connections_broker)) {
|
|
72
|
-
if (verify)
|
|
73
|
-
await rm(pending);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (verify)
|
|
77
|
-
throw Error("The Connections network permission was not confirmed. The installation remains unavailable.");
|
|
78
|
-
await writePrivate(base, JSON.stringify(update(observed.policy)));
|
|
79
|
-
await run(state.input.openshellCli, ["policy", "set", name, "--gateway", name, "--policy", base], { env });
|
|
80
|
-
}
|