open-managed-agents 0.1.0
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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
// Egress policy as data + resolution (plan 0117c; ADR 0016 §2, §3, §6).
|
|
2
|
+
//
|
|
3
|
+
// A session's egress policy is a plain serializable object parsed from the
|
|
4
|
+
// environment's `config.networking`, resolved into (a) per-session sentinels
|
|
5
|
+
// for the sandbox environment and (b) the hook set `createEgressProxy`
|
|
6
|
+
// consumes. Policy is testable without a running proxy.
|
|
7
|
+
//
|
|
8
|
+
// Postures locked in here:
|
|
9
|
+
// - default deny: no `networking` config -> no policy -> no proxy at all
|
|
10
|
+
// (the environment stays at --network none, ADR 0016 §2);
|
|
11
|
+
// - strict parse: unknown keys are rejected — a typo must not silently
|
|
12
|
+
// widen policy;
|
|
13
|
+
// - inject grants are path-scoped (pathPrefix REQUIRED) and optionally
|
|
14
|
+
// method-scoped (ADR 0016 §6 — reflectivity is per-endpoint);
|
|
15
|
+
// - secrets never enter the sandbox: the sandbox gets a random per-session
|
|
16
|
+
// sentinel; the proxy substitutes the real secret at the boundary, lazily
|
|
17
|
+
// revealed per request (rotation is picked up, no long-lived plaintext);
|
|
18
|
+
// - injection hooks NEVER throw: a reveal failure strips the credential
|
|
19
|
+
// header (fail closed for the secret) instead of crashing the proxy;
|
|
20
|
+
// - opaque tunnels (no inspection, no injection) are per-host opt-ins.
|
|
21
|
+
import { randomBytes } from "node:crypto";
|
|
22
|
+
import { isIP } from "node:net";
|
|
23
|
+
export class EgressPolicyError extends Error {
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Parsing (strict)
|
|
27
|
+
const ALLOW_KEYS = new Set(["host", "port", "pathPrefix", "opaqueTunnel"]);
|
|
28
|
+
const CREDENTIAL_KEYS = new Set([
|
|
29
|
+
"secret",
|
|
30
|
+
"env",
|
|
31
|
+
"host",
|
|
32
|
+
"port",
|
|
33
|
+
"pathPrefix",
|
|
34
|
+
"methods",
|
|
35
|
+
"header",
|
|
36
|
+
]);
|
|
37
|
+
const NETWORKING_KEYS = new Set(["allow", "credentials"]);
|
|
38
|
+
const HOSTED_NETWORKING_KEYS = new Set([
|
|
39
|
+
"type",
|
|
40
|
+
"allowed_hosts",
|
|
41
|
+
"allow_package_managers",
|
|
42
|
+
"allow_mcp_servers",
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Parse `config.networking` into OMA's internal egress policy.
|
|
46
|
+
*
|
|
47
|
+
* The public networking field has two deliberately disjoint shapes:
|
|
48
|
+
* OMA-native `{ allow, credentials }` and the CMA-shaped hosted
|
|
49
|
+
* `{ type, allowed_hosts, ... }`. Every present shape is classified and
|
|
50
|
+
* validated; an unknown or mixed shape is never treated as absent networking.
|
|
51
|
+
* Returns undefined only for absent networking or a valid hosted empty list.
|
|
52
|
+
*/
|
|
53
|
+
export function parseNetworkingConfig(config) {
|
|
54
|
+
const networking = config["networking"];
|
|
55
|
+
if (networking === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!isPlainObject(networking)) {
|
|
58
|
+
throw new EgressPolicyError("networking must be an object");
|
|
59
|
+
}
|
|
60
|
+
if (isHostedNetworkingShape(networking)) {
|
|
61
|
+
return parseHostedNetworkingConfig(networking);
|
|
62
|
+
}
|
|
63
|
+
if ("allow" in networking || "credentials" in networking) {
|
|
64
|
+
return parseNativeNetworkingConfig(networking);
|
|
65
|
+
}
|
|
66
|
+
throw new EgressPolicyError('networking must use either the OMA-native {allow, credentials} shape or the hosted {type, allowed_hosts} shape');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate networking and return the durable canonical representation.
|
|
70
|
+
*
|
|
71
|
+
* Native OMA policies are already structured records and are preserved after
|
|
72
|
+
* validation. CMA-shaped host lists are normalized to lowercase using the
|
|
73
|
+
* exact parser that runtime enforcement uses, so the console/API never show a
|
|
74
|
+
* different policy from the one the proxy will enforce.
|
|
75
|
+
*/
|
|
76
|
+
export function canonicalizeNetworkingConfig(config) {
|
|
77
|
+
const networking = config["networking"];
|
|
78
|
+
if (networking === undefined)
|
|
79
|
+
return config;
|
|
80
|
+
if (!isPlainObject(networking)) {
|
|
81
|
+
throw new EgressPolicyError("networking must be an object");
|
|
82
|
+
}
|
|
83
|
+
if (isHostedNetworkingShape(networking)) {
|
|
84
|
+
const policy = parseHostedNetworkingConfig(networking);
|
|
85
|
+
return {
|
|
86
|
+
...config,
|
|
87
|
+
networking: {
|
|
88
|
+
...networking,
|
|
89
|
+
allowed_hosts: policy?.allow.map((entry) => entry.host) ?? [],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
parseNativeNetworkingConfig(networking);
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
export function canonicalizeHostedAllowedHosts(input) {
|
|
97
|
+
const policy = parseHostedNetworkingConfig({
|
|
98
|
+
type: "limited",
|
|
99
|
+
allowed_hosts: input,
|
|
100
|
+
});
|
|
101
|
+
return policy?.allow.map((entry) => entry.host) ?? [];
|
|
102
|
+
}
|
|
103
|
+
function isHostedNetworkingShape(networking) {
|
|
104
|
+
return ("type" in networking ||
|
|
105
|
+
"allowed_hosts" in networking ||
|
|
106
|
+
"allow_package_managers" in networking ||
|
|
107
|
+
"allow_mcp_servers" in networking);
|
|
108
|
+
}
|
|
109
|
+
function parseHostedNetworkingConfig(networking) {
|
|
110
|
+
rejectUnknownKeys(networking, HOSTED_NETWORKING_KEYS, "networking");
|
|
111
|
+
const type = networking["type"];
|
|
112
|
+
if (type === "unrestricted") {
|
|
113
|
+
throw new EgressPolicyError('networking.type "unrestricted" is not supported; use type "limited" with an allowed_hosts list');
|
|
114
|
+
}
|
|
115
|
+
if (type !== "limited") {
|
|
116
|
+
throw new EgressPolicyError('networking.type must be "limited" ("unrestricted" is not supported)');
|
|
117
|
+
}
|
|
118
|
+
const rawAllowedHosts = networking["allowed_hosts"];
|
|
119
|
+
if (!Array.isArray(rawAllowedHosts)) {
|
|
120
|
+
throw new EgressPolicyError("networking.allowed_hosts must be an array");
|
|
121
|
+
}
|
|
122
|
+
for (const flag of ["allow_package_managers", "allow_mcp_servers"]) {
|
|
123
|
+
const value = networking[flag];
|
|
124
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
125
|
+
throw new EgressPolicyError(`networking.${flag} must be a boolean`);
|
|
126
|
+
}
|
|
127
|
+
if (value === true) {
|
|
128
|
+
throw new EgressPolicyError(`networking.${flag}=true is not supported by this deployment`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const allow = [];
|
|
133
|
+
rawAllowedHosts.forEach((value, index) => {
|
|
134
|
+
const host = parseHostedHost(value, `networking.allowed_hosts[${index}]`);
|
|
135
|
+
if (seen.has(host)) {
|
|
136
|
+
throw new EgressPolicyError(`networking.allowed_hosts: duplicate entry ${host}`);
|
|
137
|
+
}
|
|
138
|
+
seen.add(host);
|
|
139
|
+
// Hosted limited networking is explicitly HTTPS-only. Termination is what
|
|
140
|
+
// lets the proxy distinguish TLS from arbitrary plaintext sent through a
|
|
141
|
+
// CONNECT tunnel; no credential or path grant is synthesized here.
|
|
142
|
+
allow.push({ host, port: 443, protocol: "https", opaqueTunnel: false });
|
|
143
|
+
});
|
|
144
|
+
// A hosted empty list is an explicit, valid default-deny request. It must
|
|
145
|
+
// remain distinct from native `{ allow: [] }`, whose existing OMA behavior
|
|
146
|
+
// returns an empty policy object and therefore still requires egress wiring.
|
|
147
|
+
if (allow.length === 0)
|
|
148
|
+
return undefined;
|
|
149
|
+
return { allow, credentials: [] };
|
|
150
|
+
}
|
|
151
|
+
function parseNativeNetworkingConfig(networking) {
|
|
152
|
+
rejectUnknownKeys(networking, NETWORKING_KEYS, "networking");
|
|
153
|
+
const rawAllow = networking["allow"];
|
|
154
|
+
if (!Array.isArray(rawAllow)) {
|
|
155
|
+
throw new EgressPolicyError("networking.allow must be an array");
|
|
156
|
+
}
|
|
157
|
+
const allow = rawAllow.map((entry, i) => parseAllowEntry(entry, i));
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const entry of allow) {
|
|
160
|
+
const key = `${entry.host}:${entry.port}`;
|
|
161
|
+
if (seen.has(key)) {
|
|
162
|
+
throw new EgressPolicyError(`networking.allow: duplicate entry ${key}`);
|
|
163
|
+
}
|
|
164
|
+
seen.add(key);
|
|
165
|
+
}
|
|
166
|
+
const rawCredentials = networking["credentials"] ?? [];
|
|
167
|
+
if (!Array.isArray(rawCredentials)) {
|
|
168
|
+
throw new EgressPolicyError("networking.credentials must be an array");
|
|
169
|
+
}
|
|
170
|
+
const credentials = rawCredentials.map((entry, i) => parseCredentialGrant(entry, i));
|
|
171
|
+
const envSeen = new Set();
|
|
172
|
+
for (const grant of credentials) {
|
|
173
|
+
// Every inject host must be an allowlisted, TERMINATED host: injection
|
|
174
|
+
// happens on the decrypted leg, so an opaque host can't carry a grant.
|
|
175
|
+
const target = allow.find((entry) => entry.host === grant.host && entry.port === grant.port);
|
|
176
|
+
if (!target) {
|
|
177
|
+
throw new EgressPolicyError(`networking.credentials: ${grant.host}:${grant.port} is not in networking.allow`);
|
|
178
|
+
}
|
|
179
|
+
if (target.opaqueTunnel) {
|
|
180
|
+
throw new EgressPolicyError(`networking.credentials: ${grant.host}:${grant.port} is an opaqueTunnel host — ` +
|
|
181
|
+
"injection requires TLS termination");
|
|
182
|
+
}
|
|
183
|
+
if (envSeen.has(grant.env)) {
|
|
184
|
+
throw new EgressPolicyError(`networking.credentials: duplicate env ${grant.env}`);
|
|
185
|
+
}
|
|
186
|
+
envSeen.add(grant.env);
|
|
187
|
+
}
|
|
188
|
+
return { allow, credentials };
|
|
189
|
+
}
|
|
190
|
+
function parseAllowEntry(value, index) {
|
|
191
|
+
const at = `networking.allow[${index}]`;
|
|
192
|
+
if (!isPlainObject(value)) {
|
|
193
|
+
throw new EgressPolicyError(`${at} must be an object`);
|
|
194
|
+
}
|
|
195
|
+
rejectUnknownKeys(value, ALLOW_KEYS, at);
|
|
196
|
+
const host = parseHost(value["host"], at);
|
|
197
|
+
const port = parsePort(value["port"], at);
|
|
198
|
+
const opaqueTunnel = value["opaqueTunnel"] ?? false;
|
|
199
|
+
if (typeof opaqueTunnel !== "boolean") {
|
|
200
|
+
throw new EgressPolicyError(`${at}.opaqueTunnel must be a boolean`);
|
|
201
|
+
}
|
|
202
|
+
const entry = { host, port, opaqueTunnel };
|
|
203
|
+
if (value["pathPrefix"] !== undefined) {
|
|
204
|
+
if (opaqueTunnel) {
|
|
205
|
+
throw new EgressPolicyError(`${at}: pathPrefix cannot apply to an opaqueTunnel host (no inspection)`);
|
|
206
|
+
}
|
|
207
|
+
entry.pathPrefix = parsePathPrefix(value["pathPrefix"], at);
|
|
208
|
+
}
|
|
209
|
+
return entry;
|
|
210
|
+
}
|
|
211
|
+
function parseCredentialGrant(value, index) {
|
|
212
|
+
const at = `networking.credentials[${index}]`;
|
|
213
|
+
if (!isPlainObject(value)) {
|
|
214
|
+
throw new EgressPolicyError(`${at} must be an object`);
|
|
215
|
+
}
|
|
216
|
+
rejectUnknownKeys(value, CREDENTIAL_KEYS, at);
|
|
217
|
+
const secret = value["secret"];
|
|
218
|
+
if (typeof secret !== "string" || secret === "") {
|
|
219
|
+
throw new EgressPolicyError(`${at}.secret must be a non-empty string`);
|
|
220
|
+
}
|
|
221
|
+
const env = value["env"];
|
|
222
|
+
if (typeof env !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(env)) {
|
|
223
|
+
throw new EgressPolicyError(`${at}.env must be an UPPER_SNAKE_CASE env var name`);
|
|
224
|
+
}
|
|
225
|
+
const grant = {
|
|
226
|
+
secret,
|
|
227
|
+
env,
|
|
228
|
+
host: parseHost(value["host"], at),
|
|
229
|
+
port: parsePort(value["port"], at),
|
|
230
|
+
// ADR 0016 §6: pathPrefix is mandatory for inject grants.
|
|
231
|
+
pathPrefix: parsePathPrefix(value["pathPrefix"], at),
|
|
232
|
+
header: parseHeader(value["header"], at),
|
|
233
|
+
};
|
|
234
|
+
if (value["methods"] !== undefined) {
|
|
235
|
+
const methods = value["methods"];
|
|
236
|
+
if (!Array.isArray(methods) ||
|
|
237
|
+
methods.length === 0 ||
|
|
238
|
+
!methods.every((m) => typeof m === "string" && /^[A-Za-z]+$/.test(m))) {
|
|
239
|
+
throw new EgressPolicyError(`${at}.methods must be a non-empty array of HTTP method names`);
|
|
240
|
+
}
|
|
241
|
+
grant.methods = methods.map((m) => m.toUpperCase());
|
|
242
|
+
}
|
|
243
|
+
return grant;
|
|
244
|
+
}
|
|
245
|
+
function parseHost(value, at) {
|
|
246
|
+
if (typeof value !== "string" ||
|
|
247
|
+
value === "" ||
|
|
248
|
+
value !== value.toLowerCase() ||
|
|
249
|
+
value.includes("*") ||
|
|
250
|
+
value.endsWith(".")) {
|
|
251
|
+
throw new EgressPolicyError(`${at}.host must be a lowercase hostname (no wildcards, no trailing dot)`);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* CMA-hosted hostname grammar. This is intentionally stricter than the
|
|
257
|
+
* pre-existing native OMA parser: hosted values are a closed, safe subset and
|
|
258
|
+
* are normalized only for the internal policy, never in persisted JSON.
|
|
259
|
+
*/
|
|
260
|
+
function parseHostedHost(value, at) {
|
|
261
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
262
|
+
throw new EgressPolicyError(`${at} must be a hostname`);
|
|
263
|
+
}
|
|
264
|
+
const wildcard = value.startsWith("*.");
|
|
265
|
+
const hostname = wildcard ? value.slice(2) : value;
|
|
266
|
+
if (value.includes("*") && !wildcard) {
|
|
267
|
+
throw new EgressPolicyError(`${at} must use only a leading *. wildcard`);
|
|
268
|
+
}
|
|
269
|
+
if (value.length > 253 || hostname.length === 0 || isIP(hostname) !== 0) {
|
|
270
|
+
throw new EgressPolicyError(`${at} is not a valid DNS hostname`);
|
|
271
|
+
}
|
|
272
|
+
const labels = hostname.split(".");
|
|
273
|
+
if (labels.length < 2) {
|
|
274
|
+
throw new EgressPolicyError(`${at} must contain at least two hostname labels`);
|
|
275
|
+
}
|
|
276
|
+
for (const label of labels) {
|
|
277
|
+
if (!isHostnameLabel(label)) {
|
|
278
|
+
throw new EgressPolicyError(`${at} contains an invalid hostname label`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return `${wildcard ? "*." : ""}${hostname.toLowerCase()}`;
|
|
282
|
+
}
|
|
283
|
+
function parsePort(value, at) {
|
|
284
|
+
if (value === undefined)
|
|
285
|
+
return 443;
|
|
286
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 65535) {
|
|
287
|
+
throw new EgressPolicyError(`${at}.port must be an integer in 1..65535`);
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function parsePathPrefix(value, at) {
|
|
292
|
+
if (typeof value !== "string" || !value.startsWith("/")) {
|
|
293
|
+
throw new EgressPolicyError(`${at}.pathPrefix must be a string starting with "/"`);
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function parseHeader(value, at) {
|
|
298
|
+
if (value === undefined)
|
|
299
|
+
return "authorization";
|
|
300
|
+
if (typeof value !== "string" || !/^[a-z0-9-]+$/.test(value)) {
|
|
301
|
+
throw new EgressPolicyError(`${at}.header must be a lowercase header name`);
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
function isPlainObject(value) {
|
|
306
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
307
|
+
}
|
|
308
|
+
function rejectUnknownKeys(value, known, at) {
|
|
309
|
+
for (const key of Object.keys(value)) {
|
|
310
|
+
if (!known.has(key)) {
|
|
311
|
+
throw new EgressPolicyError(`${at}: unknown key "${key}"`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Path matching
|
|
317
|
+
/**
|
|
318
|
+
* Segment-boundary prefix match with encoding hardening (probed):
|
|
319
|
+
* - the URL parser normalizes `..` and `%2e%2e` dot segments, but
|
|
320
|
+
* `..%2f` / `%2f..` survive in pathname — an upstream that decodes %2f
|
|
321
|
+
* would escape the prefix, so any decoded form containing a ".." path
|
|
322
|
+
* step is denied outright;
|
|
323
|
+
* - `/repos` matches `/repos` and `/repos/x`, never `/repositories`.
|
|
324
|
+
*/
|
|
325
|
+
export function pathWithinPrefix(pathname, prefix) {
|
|
326
|
+
let decoded;
|
|
327
|
+
try {
|
|
328
|
+
decoded = decodeURIComponent(pathname);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return false; // malformed percent-encoding: fail closed
|
|
332
|
+
}
|
|
333
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(decoded))
|
|
334
|
+
return false;
|
|
335
|
+
if (pathname === prefix)
|
|
336
|
+
return true;
|
|
337
|
+
const boundary = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
338
|
+
return pathname.startsWith(boundary);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve an environment's networking config into per-session egress: the
|
|
342
|
+
* sandbox env (sentinels) and the proxy hook set. Returns undefined when the
|
|
343
|
+
* environment grants no egress (no proxy is stood up; --network none).
|
|
344
|
+
*/
|
|
345
|
+
export function resolveSessionEgress(opts) {
|
|
346
|
+
const policy = parseNetworkingConfig(opts.environmentConfig);
|
|
347
|
+
if (policy === undefined)
|
|
348
|
+
return undefined;
|
|
349
|
+
const { grants, sandboxEnv } = mintSentinelGrants(policy);
|
|
350
|
+
return {
|
|
351
|
+
policy,
|
|
352
|
+
sandboxEnv,
|
|
353
|
+
hooks: buildHooks(policy, grants, opts.revealSecret),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function mintSentinelGrants(policy) {
|
|
357
|
+
const grants = policy.credentials.map((grant) => ({
|
|
358
|
+
...grant,
|
|
359
|
+
sentinel: `oma-sentinel-${randomBytes(16).toString("hex")}`,
|
|
360
|
+
}));
|
|
361
|
+
const sandboxEnv = {};
|
|
362
|
+
for (const grant of grants) {
|
|
363
|
+
sandboxEnv[grant.env] = grant.sentinel;
|
|
364
|
+
}
|
|
365
|
+
return { grants, sandboxEnv };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Control-plane side of the sidecar seam (0117d). Parses the env's networking
|
|
369
|
+
* config, mints per-session sentinels, and resolves the session's granted
|
|
370
|
+
* secrets ONCE into a serializable {@link SessionEgressBundle}. Returns
|
|
371
|
+
* undefined when the environment grants no egress (no sidecar; --network none).
|
|
372
|
+
*
|
|
373
|
+
* A grant whose secret cannot be revealed is simply absent from `secrets` —
|
|
374
|
+
* the sidecar's injection hook strips that credential header (fail closed),
|
|
375
|
+
* exactly as the in-process path does.
|
|
376
|
+
*/
|
|
377
|
+
export function resolveSessionEgressBundle(opts) {
|
|
378
|
+
// The token rides in a proxy-URL userinfo slot; constrain it to a URL-safe
|
|
379
|
+
// charset so a delimiter can't malform the URL or alter the credential the
|
|
380
|
+
// sandbox client sends. OMA mints the token, so this is a contract, not a
|
|
381
|
+
// parser — callers should pass hex / base64url.
|
|
382
|
+
if (typeof opts.proxyAuthToken !== "string" ||
|
|
383
|
+
!/^[A-Za-z0-9._~-]+$/.test(opts.proxyAuthToken)) {
|
|
384
|
+
throw new EgressPolicyError("resolveSessionEgressBundle requires a non-empty URL-safe proxyAuthToken " +
|
|
385
|
+
"([A-Za-z0-9._~-]); use hex or base64url");
|
|
386
|
+
}
|
|
387
|
+
const policy = parseNetworkingConfig(opts.environmentConfig);
|
|
388
|
+
if (policy === undefined)
|
|
389
|
+
return undefined;
|
|
390
|
+
const { grants, sandboxEnv } = mintSentinelGrants(policy);
|
|
391
|
+
const secrets = {};
|
|
392
|
+
for (const grant of grants) {
|
|
393
|
+
if (grant.secret in secrets)
|
|
394
|
+
continue; // resolve each secret once
|
|
395
|
+
let real;
|
|
396
|
+
try {
|
|
397
|
+
real = opts.revealSecret(grant.secret);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
real = undefined;
|
|
401
|
+
}
|
|
402
|
+
if (real !== undefined)
|
|
403
|
+
secrets[grant.secret] = real;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
sandboxEnv,
|
|
407
|
+
bundle: {
|
|
408
|
+
policy,
|
|
409
|
+
grants,
|
|
410
|
+
secrets,
|
|
411
|
+
proxyAuthToken: opts.proxyAuthToken,
|
|
412
|
+
listenPort: opts.listenPort,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Sidecar side of the seam: rebuild the proxy hook set from a serialized
|
|
418
|
+
* {@link SessionEgressBundle}. `revealSecret` becomes a lookup into the
|
|
419
|
+
* launch-time resolved map, and the SAME {@link buildHooks} runs — so the
|
|
420
|
+
* sidecar and the in-process path share one enforcement implementation.
|
|
421
|
+
*/
|
|
422
|
+
export function buildHooksFromBundle(bundle) {
|
|
423
|
+
return buildHooks(bundle.policy, bundle.grants, (name) => bundle.secrets[name]);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Match one policy hostname against a request hostname. A hosted `*.` entry
|
|
427
|
+
* matches one or more labels before its suffix, never the bare suffix itself.
|
|
428
|
+
* Native entries never contain wildcards, but use this same matcher so every
|
|
429
|
+
* proxy enforcement layer shares one rule.
|
|
430
|
+
*/
|
|
431
|
+
export function hostMatchesAllowPattern(pattern, host) {
|
|
432
|
+
const normalizedHost = host.toLowerCase();
|
|
433
|
+
if (!pattern.startsWith("*."))
|
|
434
|
+
return pattern === normalizedHost;
|
|
435
|
+
const suffix = pattern.slice(2);
|
|
436
|
+
if (!normalizedHost.endsWith(`.${suffix}`))
|
|
437
|
+
return false;
|
|
438
|
+
const prefix = normalizedHost.slice(0, -(suffix.length + 1));
|
|
439
|
+
return prefix.length > 0 && prefix.split(".").every(isHostnameLabel);
|
|
440
|
+
}
|
|
441
|
+
function isHostnameLabel(label) {
|
|
442
|
+
return (label.length >= 1 &&
|
|
443
|
+
label.length <= 63 &&
|
|
444
|
+
/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
|
|
445
|
+
}
|
|
446
|
+
function buildHooks(policy, grants, revealSecret) {
|
|
447
|
+
const findAllow = (host, port) => policy.allow.find((entry) => entry.port === port && hostMatchesAllowPattern(entry.host, host));
|
|
448
|
+
const grantInScope = (grant, host, port, pathname, method) => grant.host === host.toLowerCase() &&
|
|
449
|
+
grant.port === port &&
|
|
450
|
+
pathWithinPrefix(pathname, grant.pathPrefix) &&
|
|
451
|
+
(grant.methods === undefined || grant.methods.includes(method.toUpperCase()));
|
|
452
|
+
return {
|
|
453
|
+
filter: (port, host) => findAllow(host, port) !== undefined,
|
|
454
|
+
shouldTerminateTLS: (host, port) => findAllow(host, port)?.opaqueTunnel !== true,
|
|
455
|
+
// ADR 0016 §3: an uninspected byte tunnel is an explicit per-host grant.
|
|
456
|
+
allowOpaqueTunnel: (host, port) => findAllow(host, port)?.opaqueTunnel === true,
|
|
457
|
+
filterRequest: async (request, context) => {
|
|
458
|
+
const url = new URL(request.url);
|
|
459
|
+
const host = url.hostname.toLowerCase();
|
|
460
|
+
const port = url.port
|
|
461
|
+
? Number.parseInt(url.port, 10)
|
|
462
|
+
: url.protocol === "https:"
|
|
463
|
+
? 443
|
|
464
|
+
: 80;
|
|
465
|
+
const entry = findAllow(host, port);
|
|
466
|
+
if (!entry) {
|
|
467
|
+
// The connection filter already gates hosts; this fires only if the
|
|
468
|
+
// two ever disagree. Fail closed.
|
|
469
|
+
return { action: "deny", reason: `${host}:${port} is not allowlisted` };
|
|
470
|
+
}
|
|
471
|
+
if (entry.protocol !== undefined && url.protocol !== `${entry.protocol}:`) {
|
|
472
|
+
return {
|
|
473
|
+
action: "deny",
|
|
474
|
+
reason: `${host}:${port} requires ${entry.protocol.toUpperCase()} transport`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (entry.pathPrefix && !pathWithinPrefix(url.pathname, entry.pathPrefix)) {
|
|
478
|
+
return {
|
|
479
|
+
action: "deny",
|
|
480
|
+
reason: `path ${url.pathname} is outside the allowed prefix for ${host}`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// Sentinel scope enforcement (ADR 0016 §6): a request carrying a
|
|
484
|
+
// sentinel ANYWHERE outside its grant's (host, port, path, method,
|
|
485
|
+
// header) scope is denied — the agent cannot steer a granted
|
|
486
|
+
// credential to an ungranted endpoint. The vendor leg discriminator is
|
|
487
|
+
// also load-bearing: only the TLS-terminated leg runs mutateHeaders, so
|
|
488
|
+
// a sentinel on the plain request leg must fail closed before forwarding.
|
|
489
|
+
for (const grant of grants) {
|
|
490
|
+
for (const [headerName, headerValue] of request.headers) {
|
|
491
|
+
if (!headerValue.includes(grant.sentinel))
|
|
492
|
+
continue;
|
|
493
|
+
if (context.leg === "plain") {
|
|
494
|
+
return {
|
|
495
|
+
action: "deny",
|
|
496
|
+
reason: `credential ${grant.env} cannot transit the plain proxy leg`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (headerName.toLowerCase() !== grant.header) {
|
|
500
|
+
return {
|
|
501
|
+
action: "deny",
|
|
502
|
+
reason: `credential ${grant.env} used outside its ${grant.header} header`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (url.protocol !== "https:" ||
|
|
506
|
+
!grantInScope(grant, host, port, url.pathname, request.method)) {
|
|
507
|
+
return {
|
|
508
|
+
action: "deny",
|
|
509
|
+
reason: `credential ${grant.env} is not granted for ` +
|
|
510
|
+
`${request.method} ${host}:${port}${url.pathname}`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { action: "allow" };
|
|
516
|
+
},
|
|
517
|
+
// Injection at the boundary (ADR 0016 §3). Runs only on the terminated
|
|
518
|
+
// TLS leg (we never set mutateHeadersPlaintext, so a secret cannot be
|
|
519
|
+
// injected into cleartext HTTP). MUST NOT throw: forwardUpstream treats
|
|
520
|
+
// a hook throw as a hard failure, and the secret's fail-closed shape is
|
|
521
|
+
// "strip the credential header", not "kill the proxy".
|
|
522
|
+
mutateHeaders: (headers, destHost, context) => {
|
|
523
|
+
if (!context)
|
|
524
|
+
return; // no request context -> no injection
|
|
525
|
+
const pathname = pathnameOf(context.path);
|
|
526
|
+
for (const grant of grants) {
|
|
527
|
+
const value = headers[grant.header];
|
|
528
|
+
if (value === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
const values = Array.isArray(value) ? value : [value];
|
|
531
|
+
if (!values.some((v) => v.includes(grant.sentinel)))
|
|
532
|
+
continue;
|
|
533
|
+
if (pathname === undefined ||
|
|
534
|
+
!grantInScope(grant, destHost, context.port, pathname, context.method)) {
|
|
535
|
+
// filterRequest already denies off-scope sentinels; if a request
|
|
536
|
+
// reaches here anyway, never let the sentinel transit either.
|
|
537
|
+
delete headers[grant.header];
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
let real;
|
|
541
|
+
try {
|
|
542
|
+
real = revealSecret(grant.secret);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
real = undefined;
|
|
546
|
+
}
|
|
547
|
+
if (real === undefined) {
|
|
548
|
+
// Reveal failed (missing secret, rotated key): strip rather than
|
|
549
|
+
// forward the sentinel — the upstream 401 tells the agent, and
|
|
550
|
+
// neither the sentinel nor a secret leaves the boundary.
|
|
551
|
+
delete headers[grant.header];
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const replaced = values.map((v) => v.replaceAll(grant.sentinel, real));
|
|
555
|
+
headers[grant.header] = Array.isArray(value) ? replaced : replaced[0];
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
// context.path is the raw origin-form request-target; normalize through the
|
|
561
|
+
// URL parser exactly like filterRequest does, so both layers see the same
|
|
562
|
+
// pathname. Returns undefined (no injection) if the path cannot parse.
|
|
563
|
+
function pathnameOf(rawPath) {
|
|
564
|
+
try {
|
|
565
|
+
return new URL(`https://x${rawPath.startsWith("/") ? "" : "/"}${rawPath}`)
|
|
566
|
+
.pathname;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { invalidRequest } from "../errors.js";
|
|
2
|
+
import { canonicalizeHostedAllowedHosts, EgressPolicyError } from "./policy.js";
|
|
3
|
+
const DEFINITIONS = [
|
|
4
|
+
{
|
|
5
|
+
id: "offline-v1",
|
|
6
|
+
version: 1,
|
|
7
|
+
name: "Offline",
|
|
8
|
+
description: "No network access. This is the default.",
|
|
9
|
+
allowedHosts: [],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "npm-pypi-v1",
|
|
13
|
+
version: 1,
|
|
14
|
+
name: "npm + PyPI",
|
|
15
|
+
description: "Install packages from the npm and Python package registries.",
|
|
16
|
+
allowedHosts: [
|
|
17
|
+
"registry.npmjs.org",
|
|
18
|
+
"pypi.org",
|
|
19
|
+
"files.pythonhosted.org",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "github-packages-v1",
|
|
24
|
+
version: 1,
|
|
25
|
+
name: "GitHub + package registries",
|
|
26
|
+
description: "Clone and download from GitHub, npm, and PyPI.",
|
|
27
|
+
allowedHosts: [
|
|
28
|
+
"registry.npmjs.org",
|
|
29
|
+
"pypi.org",
|
|
30
|
+
"files.pythonhosted.org",
|
|
31
|
+
"github.com",
|
|
32
|
+
"api.github.com",
|
|
33
|
+
"codeload.github.com",
|
|
34
|
+
"*.githubusercontent.com",
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
export const ENVIRONMENT_NETWORKING_PRESETS = Object.freeze(DEFINITIONS.map((definition) => Object.freeze({
|
|
39
|
+
id: definition.id,
|
|
40
|
+
version: definition.version,
|
|
41
|
+
name: definition.name,
|
|
42
|
+
description: definition.description,
|
|
43
|
+
networking: Object.freeze({
|
|
44
|
+
type: "limited",
|
|
45
|
+
allowed_hosts: Object.freeze(canonicalizeHostedAllowedHosts([...definition.allowedHosts])),
|
|
46
|
+
}),
|
|
47
|
+
})));
|
|
48
|
+
export function environmentNetworkingPresetCatalog(deployment) {
|
|
49
|
+
return {
|
|
50
|
+
type: "environment_networking_presets",
|
|
51
|
+
deployment,
|
|
52
|
+
presets: ENVIRONMENT_NETWORKING_PRESETS,
|
|
53
|
+
custom: {
|
|
54
|
+
https_only: true,
|
|
55
|
+
wildcard_matches_bare_domain: false,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function validateEnvironmentNetworkingHosts(input) {
|
|
60
|
+
if (typeof input !== "object" ||
|
|
61
|
+
input === null ||
|
|
62
|
+
Array.isArray(input) ||
|
|
63
|
+
Object.keys(input).some((key) => key !== "allowed_hosts")) {
|
|
64
|
+
throw invalidRequest("Request body must contain only `allowed_hosts`");
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
allowed_hosts: canonicalizeHostedAllowedHosts(input["allowed_hosts"]),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof EgressPolicyError) {
|
|
73
|
+
throw invalidRequest(`Invalid environment networking config: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|