@clawscarf/cli 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-alpha.6",
2
+ "version": "0.1.0-alpha.7",
3
3
  "engines": {
4
4
  "node": ">=24.16.0 <25 || >=26.1.0"
5
5
  },
package/recipes/README.md CHANGED
@@ -21,7 +21,7 @@ no executable hooks, secrets or user identities.
21
21
 
22
22
  - `models` selects model ID, provider and reasoning from the bundled
23
23
  [model catalog](../deploy/models/catalog.json).
24
- - `defaults` selects resources, browser and Connections enablement.
24
+ - `defaults` selects resources, public web access, browser and Connections enablement.
25
25
  - `packs: [{ id, members }]` selects agents from the bundled [packs](../packs/README.md).
26
26
  The installer checks members and collects required credentials after settings review.
27
27
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "team-server",
3
- "version": "0.1.1",
4
- "runtime": "../../runtime/releases/0.1.0-alpha.6.json",
3
+ "version": "0.1.2",
4
+ "runtime": "../../runtime/releases/0.1.0-alpha.7.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",
@@ -15,6 +15,7 @@
15
15
  "browser": {
16
16
  "enabled": false
17
17
  },
18
+ "publicWeb": true,
18
19
  "connections": {
19
20
  "enabled": true
20
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "openclaw": {
3
- "sourceTree": "9270b569864b1036a69d3080e628b68f4f4c048a",
3
+ "sourceTree": "57d9c7394530b0468c0ccc3f163297c6669e0d5a",
4
4
  "version": "2026.9.4",
5
5
  "sourceRevision": "7bc487d39dc9e059bb9b19ea08152883022f83fe"
6
6
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.6",
4
- "sourceRevision": "3527b1e4ce8218d5b144f267cafe4115aa160b85",
3
+ "version": "0.1.0-alpha.7",
4
+ "sourceRevision": "681655cc3e931209cb56b9f6866c9d780055c709",
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:24a7ef582b70cff88e09aed5d1ef7350e3a2afcfad251590a7740bf7c8b8c28b",
13
- "companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:5821b3770d19a19cfdb9faf3ee357a47c790fcc79a43d3ba4cf59f6962dc5428",
14
- "openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:69c6fb88be2b14fa0fd5ac8b5c5274195cdb5182b256aeffe8955bbd501db0db",
15
- "relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:cf07a39071b22cb362125bc000edef8c4694b9089c075df42fdb4235cd25c12a",
12
+ "gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:05a2341c36ca49d4a83d24a5f3bffff74dd54944b9fc3e13b7a5113d9ecda5f0",
13
+ "companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:94a134f1f88eaacc785d1f0b5d7715725506f5c2e9f1d822916ada86513fb3aa",
14
+ "openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:1ed1c3db878f5dc11045442319e3ff667aaebbf98977e042ace7486aff6a5b7a",
15
+ "relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:96101feea8be108c25eb2df1847a99b4a4cab2ca632d9895b36ea665bbd5f5ac",
16
16
  "models": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf",
17
17
  "browser": {
18
- "chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:6289b18228ff6bd56a59fe4a7974299e64726696c8c7a10f42bff35a919bc9bb",
19
- "node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:daf1e454db411836e595c167f924a9c951b4e6cdd40b8891d3fbd0e7a328afbb",
20
- "dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:dd543e51e8a1c52e2165bda661c2a866bcffaf198bc961f7a537c7af8b90e244",
21
- "egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:cca3d28d58002f91a03a548032a30ae5da726bcaee1ec65062581bbfb03dcbd9"
18
+ "chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:c0b9a423829a990c805fdee6af55597ec02a9dc0a8aed35590fc43a75d5fe60f",
19
+ "node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:2102a3bb15cb8c828ae09c91271f67b7e963d8bacd21f848044782e7c33f9d42",
20
+ "dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:bc877b4bc0635a1dd4b4d3dba11e1142a38d0c1ff51bb90fc58ebf42fa57562d",
21
+ "egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:7519c3dc6c54afadf5104ae92ecc5f6af6983fb308e16e2c81b7df593a8c6f8d"
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.6/openshell-darwin-arm64"
31
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/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.6/openshell-linux-arm64"
36
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/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.6/openshell-linux-x64"
41
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/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.6/openshell-gateway-darwin-arm64"
48
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/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.6/openshell-gateway-linux-arm64"
53
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/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.6/openshell-gateway-linux-x64"
58
+ "url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.7/openshell-gateway-linux-x64"
59
59
  }
60
60
  }
61
61
  }
@@ -139,6 +139,7 @@ export const localInput = z
139
139
  .optional(),
140
140
  cpu,
141
141
  memory,
142
+ publicWeb: z.boolean().default(false),
142
143
  relayImage: image.optional(),
143
144
  browser: z
144
145
  .strictObject({
@@ -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 writePrivate(join(directory, "private/connection-policy-change.json"), JSON.stringify({ ownerId: state.ownerId, rule }));
60
+ await stageNetworkPolicyChange(directory, state, {
61
+ connections_broker: rule,
62
+ });
60
63
  }
@@ -1,4 +1,4 @@
1
- import { applyConnectionPolicy } from "./connection-policy.js";
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 applyConnectionPolicy(directory, state, env);
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 applyConnectionPolicy(directory, state, env, true);
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,120 @@
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 connectionRuleSchema = 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 publicRuleSchema = z.object({
14
+ name: z.string(),
15
+ endpoints: z.array(z.object({
16
+ ports: z.array(z.number()),
17
+ allowed_ips: z.array(z.string()),
18
+ tls: z.string(),
19
+ })),
20
+ binaries: z.array(z.object({ path: z.string() })),
21
+ });
22
+ const changesSchema = z.strictObject({
23
+ connections_broker: connectionRuleSchema.nullable().optional(),
24
+ public_web: publicRuleSchema.nullable().optional(),
25
+ });
26
+ export async function stageNetworkPolicyChange(directory, state, rules) {
27
+ const file = join(directory, "private/network-policy-change.json");
28
+ let previous = {};
29
+ try {
30
+ previous = z
31
+ .strictObject({ ownerId: z.literal(state.ownerId), rules: changesSchema })
32
+ .parse(JSON.parse(await readFile(file, "utf8"))).rules;
33
+ }
34
+ catch (error) {
35
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
36
+ throw error;
37
+ }
38
+ await writePrivate(file, JSON.stringify({
39
+ ownerId: state.ownerId,
40
+ rules: changesSchema.parse({ ...previous, ...rules }),
41
+ }));
42
+ }
43
+ const policySchema = z.looseObject({
44
+ network_policies: z.record(z.string(), z.unknown()),
45
+ });
46
+ /** Apply only explicitly selected managed rules; never regenerate an operator's other policies. */
47
+ export async function applyNetworkPolicy(directory, state, env, verify = false, command = run) {
48
+ const pending = join(directory, "private/network-policy-change.json");
49
+ let text;
50
+ try {
51
+ text = await readFile(pending, "utf8");
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
55
+ return;
56
+ throw error;
57
+ }
58
+ const change = z
59
+ .strictObject({
60
+ ownerId: z.literal(state.ownerId),
61
+ rules: changesSchema,
62
+ })
63
+ .parse(JSON.parse(text));
64
+ const base = join(directory, "private/runtime-policy.json");
65
+ const update = (policy) => {
66
+ return {
67
+ ...policy,
68
+ network_policies: Object.fromEntries([
69
+ ...Object.entries(policy.network_policies).filter(([key]) => !Object.hasOwn(change.rules, key)),
70
+ ...Object.entries(change.rules).filter(([, rule]) => rule !== null),
71
+ ]),
72
+ };
73
+ };
74
+ const matches = (policy) => Object.entries(change.rules).every(([key, rule]) => {
75
+ const observed = policy.network_policies[key];
76
+ if (rule === null)
77
+ return observed === undefined;
78
+ const parsed = (key === "public_web" ? publicRuleSchema : connectionRuleSchema).safeParse(observed);
79
+ return parsed.success && isDeepStrictEqual(parsed.data, rule);
80
+ });
81
+ const name = resourceNames(state).sandbox;
82
+ if (!(await runtimeManager(directory, state, env, command).recorded()).receipt) {
83
+ if (verify)
84
+ throw Error("The configured runtime has not been recorded.");
85
+ await writePrivate(base, JSON.stringify(update(policySchema.parse(JSON.parse(await readFile(base, "utf8"))))));
86
+ return;
87
+ }
88
+ const observed = z
89
+ .object({
90
+ sandbox: z.literal(name),
91
+ policy_source: z.literal("sandbox"),
92
+ version: z.number().int(),
93
+ active_version: z.number().int(),
94
+ status: z.string(),
95
+ policy: policySchema,
96
+ })
97
+ .parse(JSON.parse(await command(state.input.openshellCli, [
98
+ "policy",
99
+ "get",
100
+ name,
101
+ "--gateway",
102
+ name,
103
+ "--base",
104
+ "--output",
105
+ "json",
106
+ ], { env })));
107
+ if (matches(observed.policy)) {
108
+ if (verify) {
109
+ if (observed.active_version !== observed.version ||
110
+ observed.status !== "effective")
111
+ throw Error("The selected network policy has not become effective in the runtime.");
112
+ await rm(pending);
113
+ }
114
+ return;
115
+ }
116
+ if (verify)
117
+ throw Error("The selected network policy was not confirmed. The installation remains unavailable.");
118
+ await writePrivate(base, JSON.stringify(update(observed.policy)));
119
+ await command(state.input.openshellCli, ["policy", "set", name, "--gateway", name, "--policy", base], { env });
120
+ }
@@ -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
- export function initialRuntimePolicy(source, models, connections) {
7
+ import { publicWebPolicy } 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: {
@@ -50,7 +52,7 @@ export function initialRuntimePolicy(source, models, connections) {
50
52
  },
51
53
  };
52
54
  }
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);
55
+ export async function prepareRuntimePolicy(directory, models, connections, publicWeb = false) {
56
+ const policy = initialRuntimePolicy(await readFile(new URL("../../deploy/openshell/policy.yaml", import.meta.url), "utf8"), models, connections, publicWeb);
55
57
  await ensurePrivateFile(join(directory, "private/runtime-policy.json"), JSON.stringify(policy));
56
58
  }
@@ -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,176 @@
1
+ // Public unicast ranges, excluding private and special-use destinations.
2
+ // Keep aligned with the browser egress boundary in deploy/execution/network/squid.conf.
3
+ export const publicWebAddresses = [
4
+ "1.0.0.0/8",
5
+ "2.0.0.0/7",
6
+ "4.0.0.0/6",
7
+ "8.0.0.0/7",
8
+ "11.0.0.0/8",
9
+ "12.0.0.0/6",
10
+ "16.0.0.0/4",
11
+ "32.0.0.0/3",
12
+ "64.0.0.0/3",
13
+ "96.0.0.0/6",
14
+ "100.0.0.0/10",
15
+ "100.128.0.0/9",
16
+ "101.0.0.0/8",
17
+ "102.0.0.0/7",
18
+ "104.0.0.0/5",
19
+ "112.0.0.0/5",
20
+ "120.0.0.0/6",
21
+ "124.0.0.0/7",
22
+ "126.0.0.0/8",
23
+ "128.0.0.0/3",
24
+ "160.0.0.0/5",
25
+ "168.0.0.0/8",
26
+ "169.0.0.0/9",
27
+ "169.128.0.0/10",
28
+ "169.192.0.0/11",
29
+ "169.224.0.0/12",
30
+ "169.240.0.0/13",
31
+ "169.248.0.0/14",
32
+ "169.252.0.0/15",
33
+ "169.255.0.0/16",
34
+ "170.0.0.0/7",
35
+ "172.0.0.0/12",
36
+ "172.32.0.0/11",
37
+ "172.64.0.0/10",
38
+ "172.128.0.0/9",
39
+ "173.0.0.0/8",
40
+ "174.0.0.0/7",
41
+ "176.0.0.0/4",
42
+ "192.0.1.0/24",
43
+ "192.0.3.0/24",
44
+ "192.0.4.0/22",
45
+ "192.0.8.0/21",
46
+ "192.0.16.0/20",
47
+ "192.0.32.0/19",
48
+ "192.0.64.0/18",
49
+ "192.0.128.0/17",
50
+ "192.1.0.0/16",
51
+ "192.2.0.0/15",
52
+ "192.4.0.0/14",
53
+ "192.8.0.0/13",
54
+ "192.16.0.0/12",
55
+ "192.32.0.0/11",
56
+ "192.64.0.0/12",
57
+ "192.80.0.0/13",
58
+ "192.88.0.0/18",
59
+ "192.88.64.0/19",
60
+ "192.88.96.0/23",
61
+ "192.88.98.0/24",
62
+ "192.88.100.0/22",
63
+ "192.88.104.0/21",
64
+ "192.88.112.0/20",
65
+ "192.88.128.0/17",
66
+ "192.89.0.0/16",
67
+ "192.90.0.0/15",
68
+ "192.92.0.0/14",
69
+ "192.96.0.0/11",
70
+ "192.128.0.0/11",
71
+ "192.160.0.0/13",
72
+ "192.169.0.0/16",
73
+ "192.170.0.0/15",
74
+ "192.172.0.0/14",
75
+ "192.176.0.0/12",
76
+ "192.192.0.0/10",
77
+ "193.0.0.0/8",
78
+ "194.0.0.0/7",
79
+ "196.0.0.0/7",
80
+ "198.0.0.0/12",
81
+ "198.16.0.0/15",
82
+ "198.20.0.0/14",
83
+ "198.24.0.0/13",
84
+ "198.32.0.0/12",
85
+ "198.48.0.0/15",
86
+ "198.50.0.0/16",
87
+ "198.51.0.0/18",
88
+ "198.51.64.0/19",
89
+ "198.51.96.0/22",
90
+ "198.51.101.0/24",
91
+ "198.51.102.0/23",
92
+ "198.51.104.0/21",
93
+ "198.51.112.0/20",
94
+ "198.51.128.0/17",
95
+ "198.52.0.0/14",
96
+ "198.56.0.0/13",
97
+ "198.64.0.0/10",
98
+ "198.128.0.0/9",
99
+ "199.0.0.0/8",
100
+ "200.0.0.0/7",
101
+ "202.0.0.0/8",
102
+ "203.0.0.0/18",
103
+ "203.0.64.0/19",
104
+ "203.0.96.0/20",
105
+ "203.0.112.0/24",
106
+ "203.0.114.0/23",
107
+ "203.0.116.0/22",
108
+ "203.0.120.0/21",
109
+ "203.0.128.0/17",
110
+ "203.1.0.0/16",
111
+ "203.2.0.0/15",
112
+ "203.4.0.0/14",
113
+ "203.8.0.0/13",
114
+ "203.16.0.0/12",
115
+ "203.32.0.0/11",
116
+ "203.64.0.0/10",
117
+ "203.128.0.0/9",
118
+ "204.0.0.0/6",
119
+ "208.0.0.0/4",
120
+ "2000::/16",
121
+ "2001:200::/23",
122
+ "2001:400::/22",
123
+ "2001:800::/22",
124
+ "2001:c00::/24",
125
+ "2001:d00::/25",
126
+ "2001:d80::/27",
127
+ "2001:da0::/28",
128
+ "2001:db0::/29",
129
+ "2001:db9::/32",
130
+ "2001:dba::/31",
131
+ "2001:dbc::/30",
132
+ "2001:dc0::/26",
133
+ "2001:e00::/23",
134
+ "2001:1000::/20",
135
+ "2001:2000::/19",
136
+ "2001:4000::/18",
137
+ "2001:8000::/17",
138
+ "2003::/16",
139
+ "2004::/14",
140
+ "2008::/13",
141
+ "2010::/12",
142
+ "2020::/11",
143
+ "2040::/10",
144
+ "2080::/9",
145
+ "2100::/8",
146
+ "2200::/7",
147
+ "2400::/6",
148
+ "2800::/5",
149
+ "3000::/5",
150
+ "3800::/6",
151
+ "3c00::/7",
152
+ "3e00::/8",
153
+ "3f00::/9",
154
+ "3f80::/10",
155
+ "3fc0::/11",
156
+ "3fe0::/12",
157
+ "3ff0::/13",
158
+ "3ff8::/14",
159
+ "3ffc::/15",
160
+ "3ffe::/16",
161
+ "3fff:1000::/20",
162
+ "3fff:2000::/19",
163
+ "3fff:4000::/18",
164
+ "3fff:8000::/17",
165
+ ];
166
+ export function publicWebPolicy(enabled) {
167
+ return enabled
168
+ ? {
169
+ name: "Public web",
170
+ endpoints: [
171
+ { ports: [80, 443], allowed_ips: publicWebAddresses, tls: "skip" },
172
+ ],
173
+ binaries: [{ path: "/**" }],
174
+ }
175
+ : null;
176
+ }
@@ -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. You can now remove its installation folder.");
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;
@@ -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)}` : ""}\n\nManage this installation:\n clawscarf status --directory ${quote(options.directory ?? defaultInstallationDirectory)}\n clawscarf stop --directory ${quote(options.directory ?? defaultInstallationDirectory)}`
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,
@@ -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
- }