@cofy-x/axern-sdk 0.5.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/network-policy.d.ts +41 -0
- package/dist/network-policy.js +132 -0
- package/dist/proto/axern/control/capability/v1/capability.proto +5 -0
- package/dist/proto/axern/control/common/v1/common.proto +43 -0
- package/dist/sandbox/index.d.ts +2 -0
- package/dist/sandbox/index.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,32 @@ try {
|
|
|
68
68
|
}
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
## Network Policies
|
|
72
|
+
|
|
73
|
+
Omitting `networkPolicy` keeps unrestricted v0.5 behavior. Strict policies are
|
|
74
|
+
fail-closed; `denyDns` only refuses matching traditional UDP/TCP DNS queries and
|
|
75
|
+
does not block direct IP traffic, DoH, DoT, or already-resolved addresses.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { NetworkPolicy, Sandbox } from "@cofy-x/axern-sdk";
|
|
79
|
+
|
|
80
|
+
const sandbox = await new Sandbox({
|
|
81
|
+
client,
|
|
82
|
+
image: "docker.io/library/python:3.12-slim",
|
|
83
|
+
networkPolicy: NetworkPolicy.denyDns(
|
|
84
|
+
"github.com",
|
|
85
|
+
"*.github.com",
|
|
86
|
+
"githubusercontent.com",
|
|
87
|
+
"*.githubusercontent.com",
|
|
88
|
+
),
|
|
89
|
+
}).start();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`NetworkPolicy.allowDomains("example.com", "*.example.com")` allows only
|
|
93
|
+
strict HTTP/HTTPS destinations validated by DNS plus HTTP Host or TLS SNI.
|
|
94
|
+
`NetworkPolicy.strict({ cidrRules: [...] })` adds explicit TCP/UDP CIDR and port
|
|
95
|
+
grants; `NetworkPolicy.denyAll()` allows no egress.
|
|
96
|
+
|
|
71
97
|
Run a tool from a separate image with `execImage` or `processImage`. OCI and
|
|
72
98
|
Nydus refs use the same image field. When `mounts` is omitted, the SDK requests
|
|
73
99
|
`/workspace -> /workspace`; pass `mounts: []` for no shared paths. Use
|
package/dist/client/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as grpc from "@grpc/grpc-js";
|
|
7
7
|
import { NodeSandboxClient } from "../node/client.js";
|
|
8
8
|
import type { ResourceQuantity } from "../resources.js";
|
|
9
|
+
import type { NetworkPolicy } from "../network-policy.js";
|
|
9
10
|
import { TunnelControlClient } from "../tunnel/control.js";
|
|
10
11
|
import type { VolumeMount } from "../types.js";
|
|
11
12
|
import type { GatewayTransportOptions } from "../tunnel/relay.js";
|
|
@@ -33,6 +34,7 @@ export interface CreateServiceOptions {
|
|
|
33
34
|
env?: Record<string, string>;
|
|
34
35
|
cwd?: string;
|
|
35
36
|
runtimeClass?: string;
|
|
37
|
+
networkPolicy?: NetworkPolicy;
|
|
36
38
|
extensionCapabilities?: readonly ExtensionCapability[];
|
|
37
39
|
volumes?: readonly VolumeMount[];
|
|
38
40
|
requestCpu?: ResourceQuantity;
|
package/dist/client/index.js
CHANGED
|
@@ -207,6 +207,9 @@ export class AxernClient {
|
|
|
207
207
|
env: options.env ?? {},
|
|
208
208
|
cwd: options.cwd ?? "",
|
|
209
209
|
runtime_class: options.runtimeClass ?? "",
|
|
210
|
+
...(options.networkPolicy === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { network: { egress_policy: options.networkPolicy.toWire() } }),
|
|
210
213
|
extension_capability_requirements: (options.extensionCapabilities ?? []).map((capability) => ({
|
|
211
214
|
capability: { name: capability.name, value: capability.value ?? "" },
|
|
212
215
|
})),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { SandboxProcess } from "./node/process.js";
|
|
|
14
14
|
export type { ResourceQuantity } from "./resources.js";
|
|
15
15
|
export { Sandbox } from "./sandbox/index.js";
|
|
16
16
|
export type { SandboxMetadata, SandboxOptions, SandboxState } from "./sandbox/index.js";
|
|
17
|
+
export { NetworkPolicy, cidrRule, portRange } from "./network-policy.js";
|
|
18
|
+
export type { CIDRRule, PortRange, StrictNetworkPolicyOptions } from "./network-policy.js";
|
|
17
19
|
export type { ChmodOptions, CapabilityDependencyStatus, CapabilityProviderStatus, CapabilityProviderSummary, CapabilityStatus, Command, ComputerUseDependencyStatus, ComputerUseDisplay, ComputerUseKeyboardOptions, ComputerUseMouseOptions, ComputerUseRegion, ComputerUseScreenshot, ComputerUseScreenshotOptions, ComputerUseStatus, CopyOptions, DownloadArchiveOptions, DownloadDirOptions, ExecOptions, ExecResult, ImageExecOptions, ImageProcessMount, ImageProcessOptions, MkdirOptions, MoveOptions, NodeCallOptions, ProcessEvent, ProcessOptions, ProcessResult, RemoveOptions, SandboxFileInfo, SandboxFileKind, TouchOptions, TunnelConnectorOptions, TunnelMetadata, TunnelOptions, UploadArchiveOptions, UploadDirOptions, VolumeMount, WriteFileOptions, } from "./types.js";
|
|
18
20
|
export { workspaceMount } from "./types.js";
|
|
19
21
|
export { AXERN_VERSION, platformName } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export { AxernError, AxernRpcError, isNotFound, isCancelled, isUnavailable, erro
|
|
|
9
9
|
export { NodeSandboxClient } from "./node/client.js";
|
|
10
10
|
export { SandboxProcess } from "./node/process.js";
|
|
11
11
|
export { Sandbox } from "./sandbox/index.js";
|
|
12
|
+
export { NetworkPolicy, cidrRule, portRange } from "./network-policy.js";
|
|
12
13
|
export { workspaceMount } from "./types.js";
|
|
13
14
|
export { AXERN_VERSION, platformName } from "./version.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
export interface PortRange {
|
|
7
|
+
readonly start: number;
|
|
8
|
+
readonly end: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CIDRRule {
|
|
11
|
+
readonly cidr: string;
|
|
12
|
+
readonly protocol: "tcp" | "udp";
|
|
13
|
+
readonly ports: readonly PortRange[];
|
|
14
|
+
}
|
|
15
|
+
export interface StrictNetworkPolicyOptions {
|
|
16
|
+
readonly domains?: readonly string[];
|
|
17
|
+
readonly cidrRules?: readonly CIDRRule[];
|
|
18
|
+
}
|
|
19
|
+
type NetworkPolicyWire = {
|
|
20
|
+
strict: {
|
|
21
|
+
allowed_domains: string[];
|
|
22
|
+
allowed_cidrs: Record<string, unknown>[];
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
dns_deny: {
|
|
26
|
+
denied_domains: string[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare class NetworkPolicy {
|
|
30
|
+
private readonly wire;
|
|
31
|
+
private constructor();
|
|
32
|
+
static strict(options?: StrictNetworkPolicyOptions): NetworkPolicy;
|
|
33
|
+
static allowDomains(...domains: string[]): NetworkPolicy;
|
|
34
|
+
static denyDns(...domains: string[]): NetworkPolicy;
|
|
35
|
+
static denyAll(): NetworkPolicy;
|
|
36
|
+
/** @internal */
|
|
37
|
+
toWire(): NetworkPolicyWire;
|
|
38
|
+
}
|
|
39
|
+
export declare function portRange(start: number, end?: number): PortRange;
|
|
40
|
+
export declare function cidrRule(cidr: string, protocol: "tcp" | "udp", ...ports: PortRange[]): CIDRRule;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 cofy-x
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { isIP } from "node:net";
|
|
7
|
+
import { domainToASCII } from "node:url";
|
|
8
|
+
const maxRules = 256;
|
|
9
|
+
export class NetworkPolicy {
|
|
10
|
+
wire;
|
|
11
|
+
constructor(wire) {
|
|
12
|
+
this.wire = wire;
|
|
13
|
+
}
|
|
14
|
+
static strict(options = {}) {
|
|
15
|
+
const domains = normalizeDomains(options.domains ?? []);
|
|
16
|
+
const cidrRules = normalizeCIDRRules(options.cidrRules ?? []);
|
|
17
|
+
if (domains.length + cidrRules.length > maxRules) {
|
|
18
|
+
throw new RangeError(`network policy may contain at most ${maxRules} rules`);
|
|
19
|
+
}
|
|
20
|
+
return new NetworkPolicy({
|
|
21
|
+
strict: {
|
|
22
|
+
allowed_domains: domains,
|
|
23
|
+
allowed_cidrs: cidrRules.map((rule) => ({
|
|
24
|
+
cidr: rule.cidr,
|
|
25
|
+
protocol: rule.protocol === "tcp" ? 1 : 2,
|
|
26
|
+
ports: rule.ports.map((port) => ({ start: port.start, end: port.end })),
|
|
27
|
+
})),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
static allowDomains(...domains) {
|
|
32
|
+
return NetworkPolicy.strict({ domains });
|
|
33
|
+
}
|
|
34
|
+
static denyDns(...domains) {
|
|
35
|
+
const normalized = normalizeDomains(domains);
|
|
36
|
+
if (normalized.length === 0)
|
|
37
|
+
throw new RangeError("denyDns requires at least one domain");
|
|
38
|
+
return new NetworkPolicy({ dns_deny: { denied_domains: normalized } });
|
|
39
|
+
}
|
|
40
|
+
static denyAll() {
|
|
41
|
+
return NetworkPolicy.strict();
|
|
42
|
+
}
|
|
43
|
+
/** @internal */
|
|
44
|
+
toWire() {
|
|
45
|
+
return structuredClone(this.wire);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function portRange(start, end = start) {
|
|
49
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start || end > 65535) {
|
|
50
|
+
throw new RangeError("ports must be an inclusive range within 1..65535");
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({ start, end });
|
|
53
|
+
}
|
|
54
|
+
export function cidrRule(cidr, protocol, ...ports) {
|
|
55
|
+
return normalizeCIDRRule({ cidr, protocol, ports });
|
|
56
|
+
}
|
|
57
|
+
function normalizeDomains(values) {
|
|
58
|
+
const result = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const value of values) {
|
|
61
|
+
let raw = value.trim().toLowerCase();
|
|
62
|
+
const wildcard = raw.startsWith("*.");
|
|
63
|
+
if (wildcard)
|
|
64
|
+
raw = raw.slice(2);
|
|
65
|
+
raw = raw.replace(/\.+$/, "");
|
|
66
|
+
if (raw === "" || /[/:@?#*]/u.test(raw) || isIP(raw) !== 0) {
|
|
67
|
+
throw new TypeError(`invalid domain rule ${JSON.stringify(value)}`);
|
|
68
|
+
}
|
|
69
|
+
const ascii = domainToASCII(raw).toLowerCase();
|
|
70
|
+
const labels = ascii.split(".");
|
|
71
|
+
if (ascii === "" || Buffer.byteLength(ascii, "ascii") > 253 || labels.some((label) => label === "" || label.length > 63)) {
|
|
72
|
+
throw new TypeError(`invalid domain rule ${JSON.stringify(value)}`);
|
|
73
|
+
}
|
|
74
|
+
const normalized = wildcard ? `*.${ascii}` : ascii;
|
|
75
|
+
if (!seen.has(normalized)) {
|
|
76
|
+
seen.add(normalized);
|
|
77
|
+
result.push(normalized);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function normalizeCIDRRules(values) {
|
|
83
|
+
const result = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (const value of values) {
|
|
86
|
+
const rule = normalizeCIDRRule(value);
|
|
87
|
+
const key = JSON.stringify(rule);
|
|
88
|
+
if (!seen.has(key)) {
|
|
89
|
+
seen.add(key);
|
|
90
|
+
result.push(rule);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
function normalizeCIDRRule(value) {
|
|
96
|
+
const cidr = value.cidr.trim();
|
|
97
|
+
const separator = cidr.lastIndexOf("/");
|
|
98
|
+
const address = separator < 0 ? "" : cidr.slice(0, separator);
|
|
99
|
+
const prefix = separator < 0 ? -1 : Number(cidr.slice(separator + 1));
|
|
100
|
+
const family = isIP(address);
|
|
101
|
+
if ((family !== 4 && family !== 6) || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
|
|
102
|
+
throw new TypeError(`invalid CIDR ${JSON.stringify(value.cidr)}`);
|
|
103
|
+
}
|
|
104
|
+
if (isProtectedCIDR(address, prefix, family)) {
|
|
105
|
+
throw new TypeError(`CIDR ${JSON.stringify(value.cidr)} targets a protected address range`);
|
|
106
|
+
}
|
|
107
|
+
if (value.protocol !== "tcp" && value.protocol !== "udp")
|
|
108
|
+
throw new TypeError("CIDR protocol must be tcp or udp");
|
|
109
|
+
if (value.ports.length === 0)
|
|
110
|
+
throw new RangeError("CIDR rule requires at least one port range");
|
|
111
|
+
const ports = value.ports.map((port) => portRange(port.start, port.end));
|
|
112
|
+
return Object.freeze({ cidr, protocol: value.protocol, ports: Object.freeze(ports) });
|
|
113
|
+
}
|
|
114
|
+
function isProtectedCIDR(address, prefix, family) {
|
|
115
|
+
if (family === 4) {
|
|
116
|
+
const value = address.split(".").reduce((result, octet) => (result << 8n) | BigInt(octet), 0n);
|
|
117
|
+
const protectedRanges = [
|
|
118
|
+
[0n, 32],
|
|
119
|
+
[1684301000n, 32], // 100.100.100.200
|
|
120
|
+
[2130706432n, 8], // 127.0.0.0
|
|
121
|
+
[2851995648n, 16], // 169.254.0.0
|
|
122
|
+
[3221225664n, 32], // 192.0.0.192
|
|
123
|
+
[3758096384n, 4], // 224.0.0.0
|
|
124
|
+
];
|
|
125
|
+
return protectedRanges.some(([base, bits]) => prefix >= bits && (value >> BigInt(32 - bits)) === (base >> BigInt(32 - bits)));
|
|
126
|
+
}
|
|
127
|
+
const canonical = new URL(`http://[${address}]/`).hostname.slice(1, -1).toLowerCase();
|
|
128
|
+
if (prefix >= 128 && (canonical === "::" || canonical === "::1"))
|
|
129
|
+
return true;
|
|
130
|
+
const first = Number.parseInt(canonical.split(":", 1)[0] || "0", 16);
|
|
131
|
+
return (prefix >= 10 && first >= 0xfe80 && first <= 0xfebf) || (prefix >= 8 && first >= 0xff00 && first <= 0xffff);
|
|
132
|
+
}
|
|
@@ -28,6 +28,11 @@ enum PlatformCapability {
|
|
|
28
28
|
PLATFORM_CAPABILITY_RUNSC_MEMORY_ENFORCEMENT_SELF_TEST = 13;
|
|
29
29
|
PLATFORM_CAPABILITY_RUNC_EPHEMERAL_ENFORCEMENT_SELF_TEST = 14;
|
|
30
30
|
PLATFORM_CAPABILITY_RUNSC_EPHEMERAL_ENFORCEMENT_SELF_TEST = 15;
|
|
31
|
+
PLATFORM_CAPABILITY_EGRESSD_DNS_POLICY_SELF_TEST = 16;
|
|
32
|
+
PLATFORM_CAPABILITY_EGRESSD_STRICT_EGRESS_SELF_TEST = 17;
|
|
33
|
+
// Workload-facing capabilities derived from the egressd self-test facts.
|
|
34
|
+
PLATFORM_CAPABILITY_DNS_POLICY_ENFORCEMENT = 18;
|
|
35
|
+
PLATFORM_CAPABILITY_STRICT_EGRESS_ENFORCEMENT = 19;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
message ExtensionCapability {
|
|
@@ -49,6 +49,49 @@ enum NetworkMode {
|
|
|
49
49
|
|
|
50
50
|
message NetworkSpec {
|
|
51
51
|
NetworkMode mode = 1;
|
|
52
|
+
NetworkEgressPolicy egress_policy = 2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// NetworkEgressPolicy deliberately separates strict enforcement from the
|
|
56
|
+
// DNS-only convenience policy. Callers must not treat dns_deny as an egress
|
|
57
|
+
// security boundary: direct IP traffic, encrypted DNS, and already-known
|
|
58
|
+
// addresses remain outside that policy.
|
|
59
|
+
message NetworkEgressPolicy {
|
|
60
|
+
oneof policy {
|
|
61
|
+
StrictEgressPolicy strict = 1;
|
|
62
|
+
DnsDenyPolicy dns_deny = 2;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// StrictEgressPolicy is default-deny. Domain grants authorize HTTP and HTTPS
|
|
67
|
+
// only, on TCP ports 80 and 443, and require controlled DNS plus matching HTTP
|
|
68
|
+
// Host or TLS SNI. Other protocols require an explicit CIDR rule.
|
|
69
|
+
message StrictEgressPolicy {
|
|
70
|
+
repeated string allowed_domains = 1;
|
|
71
|
+
repeated CIDREgressRule allowed_cidrs = 2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// DnsDenyPolicy returns REFUSED for matching conventional UDP/TCP DNS queries.
|
|
75
|
+
// It does not block direct IP traffic, DoH, DoT, or previously resolved IPs.
|
|
76
|
+
message DnsDenyPolicy {
|
|
77
|
+
repeated string denied_domains = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
enum EgressProtocol {
|
|
81
|
+
EGRESS_PROTOCOL_UNSPECIFIED = 0;
|
|
82
|
+
EGRESS_PROTOCOL_TCP = 1;
|
|
83
|
+
EGRESS_PROTOCOL_UDP = 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
message PortRange {
|
|
87
|
+
uint32 start = 1;
|
|
88
|
+
uint32 end = 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
message CIDREgressRule {
|
|
92
|
+
string cidr = 1;
|
|
93
|
+
EgressProtocol protocol = 2;
|
|
94
|
+
repeated PortRange ports = 3;
|
|
52
95
|
}
|
|
53
96
|
|
|
54
97
|
message PlacementConstraints {
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { AxernClient } from "../client/index.js";
|
|
|
7
7
|
import type { ExtensionCapability } from "../client/index.js";
|
|
8
8
|
import type { SandboxProcess } from "../node/process.js";
|
|
9
9
|
import type { ResourceQuantity } from "../resources.js";
|
|
10
|
+
import type { NetworkPolicy } from "../network-policy.js";
|
|
10
11
|
import type { ChmodOptions, CapabilityStatus, Command, ComputerUseDisplay, ComputerUseKeyboardOptions, ComputerUseMouseOptions, ComputerUseScreenshot, ComputerUseScreenshotOptions, ComputerUseStatus, CopyOptions, DownloadDirOptions, ExecOptions, ExecResult, ImageExecOptions, ImageProcessOptions, MkdirOptions, MoveOptions, NodeCallOptions, ProcessOptions, RemoveOptions, SandboxFileInfo, TunnelMetadata, TunnelOptions, TouchOptions, UploadDirOptions, VolumeMount, WriteFileOptions } from "../types.js";
|
|
11
12
|
export interface SandboxOptions {
|
|
12
13
|
client: AxernClient;
|
|
@@ -18,6 +19,7 @@ export interface SandboxOptions {
|
|
|
18
19
|
env?: Record<string, string>;
|
|
19
20
|
cwd?: string;
|
|
20
21
|
runtimeClass?: string;
|
|
22
|
+
networkPolicy?: NetworkPolicy;
|
|
21
23
|
extensionCapabilities?: readonly ExtensionCapability[];
|
|
22
24
|
volumes?: readonly VolumeMount[];
|
|
23
25
|
requestCpu?: ResourceQuantity;
|
package/dist/sandbox/index.js
CHANGED
|
@@ -59,6 +59,7 @@ export class Sandbox {
|
|
|
59
59
|
env: this.options.env,
|
|
60
60
|
cwd: this.options.cwd,
|
|
61
61
|
runtimeClass: this.options.runtimeClass,
|
|
62
|
+
networkPolicy: this.options.networkPolicy,
|
|
62
63
|
extensionCapabilities: this.options.extensionCapabilities,
|
|
63
64
|
volumes: this.options.volumes,
|
|
64
65
|
requestCpu: this.options.requestCpu,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED