@themoltnet/sandbox-gondolin 0.1.0 → 0.3.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/README.md +67 -2
- package/dist/index.d.ts +124 -1
- package/dist/index.js +376 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ implementation can be added beside it without going through Pi.
|
|
|
20
20
|
(`assertGuestEnvironmentBoundary`).
|
|
21
21
|
- `VmConfig.brokeredSecrets` — host-brokered secrets: the guest sees a
|
|
22
22
|
placeholder, Gondolin's `SecretManager` substitutes the value only in
|
|
23
|
-
requests to the declared host patterns.
|
|
23
|
+
requests to the declared protocol, host patterns, and ports.
|
|
24
24
|
- `ensureSnapshot` — build and cache the base checkpoint.
|
|
25
25
|
- Small helpers: `findMainWorktree`, `isResolvedPathInsideRoot`,
|
|
26
26
|
`abortableResource` / `throwIfAborted`.
|
|
@@ -36,11 +36,76 @@ implementation can be added beside it without going through Pi.
|
|
|
36
36
|
|
|
37
37
|
## Known boundary facts
|
|
38
38
|
|
|
39
|
-
-
|
|
39
|
+
- Network egress policy is hostname-granular. Brokered credentials add a
|
|
40
|
+
separately attested protocol and port boundary.
|
|
40
41
|
- Guest `127.0.0.1` / `localhost` never leave the VM; names are resolved on
|
|
41
42
|
the host side by the proxy.
|
|
42
43
|
- Exec abort only drops the host session; a caller that needs the guest
|
|
43
44
|
process gone must kill it in the guest.
|
|
44
45
|
|
|
46
|
+
## Brokered HTTP secrets
|
|
47
|
+
|
|
48
|
+
`brokeredSecrets` is trusted-host input to `resumeVm`; it is deliberately not
|
|
49
|
+
part of `SandboxConfig`. A remotely stored runtime profile may constrain
|
|
50
|
+
network access and refer to a logical credential requirement, but it must not
|
|
51
|
+
contain a value or choose a host secret-provider coordinate.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const managed = await resumeVm({
|
|
55
|
+
checkpointPath,
|
|
56
|
+
agentName: 'worker',
|
|
57
|
+
guestCredentialMode: 'host-authenticated',
|
|
58
|
+
mountPath: workspace,
|
|
59
|
+
sandboxConfig: {
|
|
60
|
+
network: { allowedHosts: ['api.example.com'] },
|
|
61
|
+
},
|
|
62
|
+
brokeredSecrets: [
|
|
63
|
+
{
|
|
64
|
+
id: 'example-api',
|
|
65
|
+
guestEnv: 'EXAMPLE_API_TOKEN',
|
|
66
|
+
hosts: ['api.example.com'],
|
|
67
|
+
value: await trustedHostSecretProvider.get('example-api'),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Inside the VM, `$EXAMPLE_API_TOKEN` is a random Gondolin placeholder. A normal
|
|
74
|
+
guest command can use it in an HTTP header:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -fsS \
|
|
78
|
+
-H "Authorization: Bearer $EXAMPLE_API_TOKEN" \
|
|
79
|
+
https://api.example.com/v1/items
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The host proxy substitutes the real value only for the declared origin. HTTPS
|
|
83
|
+
on port 443 is the default. Plain HTTP requires an explicit `protocol: 'http'`
|
|
84
|
+
and should be limited to an exact port for a controlled local fixture.
|
|
85
|
+
Preflight fails before VM resume when the binding is missing, an environment
|
|
86
|
+
name collides with another guest source, or a credential host is outside the
|
|
87
|
+
effective network policy. Values are not substituted in request bodies or URL
|
|
88
|
+
queries. Gondolin also decodes, substitutes, and re-encodes HTTP Basic
|
|
89
|
+
authorization, covering the password encoding used by HTTPS Git credential
|
|
90
|
+
helpers; OAuth client secrets sent in form bodies remain host-only.
|
|
91
|
+
|
|
92
|
+
Rotation and revocation do not require exposing or changing the guest
|
|
93
|
+
placeholder:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
managed.secretManager.rotateSecret('EXAMPLE_API_TOKEN', rotated);
|
|
97
|
+
managed.secretManager.revokeSecret('EXAMPLE_API_TOKEN');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Keep signing keys, GitHub App private keys, SSH keys, and other non-HTTP
|
|
101
|
+
credentials out of this channel. They require narrow host capabilities rather
|
|
102
|
+
than bearer-secret substitution.
|
|
103
|
+
|
|
104
|
+
The portable boundary is the sequence _requirement → trusted local binding →
|
|
105
|
+
resolved delivery_. A Docker sandbox can implement the same sequence with its
|
|
106
|
+
native secret broker; hostname rewriting, placeholder lifecycle, and other
|
|
107
|
+
provider behavior remain adapter-specific, while the attested protocol, host,
|
|
108
|
+
and port boundary must remain equivalent.
|
|
109
|
+
|
|
45
110
|
Live tests (`vm-manager.integration.test.ts`) boot real VMs and run only with
|
|
46
111
|
`MOLTNET_PI_VM_INTEGRATION=1`; CI runs them in the agent-daemon Core lane.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MemoryProvider } from '@earendil-works/gondolin';
|
|
2
|
+
import { SecretDefinition } from '@earendil-works/gondolin';
|
|
2
3
|
import { VirtualFileHandle } from '@earendil-works/gondolin';
|
|
3
4
|
import { VM } from '@earendil-works/gondolin';
|
|
4
5
|
|
|
@@ -38,6 +39,52 @@ export declare class AutoParentMemoryProvider extends MemoryProvider {
|
|
|
38
39
|
openSync(pathname: string, flags: string, mode?: number): VirtualFileHandle;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** Per-attempt host binding. The value must never be persisted or evidenced. */
|
|
43
|
+
export declare interface BrokeredHttpSecretBinding extends BrokeredHttpSecretDescriptor {
|
|
44
|
+
/** Resolved only by trusted host code immediately before VM resume. */
|
|
45
|
+
value?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare class BrokeredHttpSecretBoundaryError extends Error {
|
|
49
|
+
readonly issues: readonly string[];
|
|
50
|
+
constructor(issues: readonly string[]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Value-free declaration of one HTTP credential delivered by the Gondolin
|
|
55
|
+
* host proxy. This is an adapter contract, not a persisted runtime-profile
|
|
56
|
+
* schema: trusted host code resolves the matching value for each attempt.
|
|
57
|
+
*/
|
|
58
|
+
export declare interface BrokeredHttpSecretDescriptor {
|
|
59
|
+
/** Stable, evidence-safe logical requirement id. */
|
|
60
|
+
id: string;
|
|
61
|
+
/** Guest variable that receives an opaque Gondolin placeholder. */
|
|
62
|
+
guestEnv: string;
|
|
63
|
+
/** Hostname patterns to which the proxy may send the resolved value. */
|
|
64
|
+
hosts: readonly string[];
|
|
65
|
+
/** Attested upstream transport. Defaults to HTTPS. */
|
|
66
|
+
protocol?: 'https' | 'http';
|
|
67
|
+
/** Attested upstream ports. Defaults to 443 for HTTPS and 80 for HTTP. */
|
|
68
|
+
ports?: readonly number[];
|
|
69
|
+
/** Missing values fail preflight unless explicitly optional. */
|
|
70
|
+
required?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Host-only capabilities that cannot widen a validated destination grant. */
|
|
74
|
+
export declare interface BrokeredHttpSecretManager {
|
|
75
|
+
/** Rotate a configured secret value without changing its destination set. */
|
|
76
|
+
rotateSecret(guestEnv: string, value: string): void;
|
|
77
|
+
/** Revoke a configured secret for the remainder of this VM lifetime. */
|
|
78
|
+
revokeSecret(guestEnv: string): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Canonicalize and validate the value-free portion of a brokered binding.
|
|
83
|
+
* Runtime attestation and Gondolin enforcement both consume this function so
|
|
84
|
+
* the evidenced destination set cannot drift from the enforced one.
|
|
85
|
+
*/
|
|
86
|
+
export declare function canonicalizeBrokeredHttpSecretDescriptor(descriptor: BrokeredHttpSecretDescriptor): Required<BrokeredHttpSecretDescriptor>;
|
|
87
|
+
|
|
41
88
|
export declare function delay(ms: number, signal: AbortSignal | undefined, label: string): Promise<void>;
|
|
42
89
|
|
|
43
90
|
/**
|
|
@@ -62,6 +109,9 @@ export declare function findMainWorktree(startPath?: string): string;
|
|
|
62
109
|
/** Commands guaranteed by the base Gondolin snapshot. */
|
|
63
110
|
export declare const GONDOLIN_BASE_EXECUTABLES: readonly string[];
|
|
64
111
|
|
|
112
|
+
/** Guest directory holding one PID file per projected service. */
|
|
113
|
+
export declare const GUEST_SERVICE_PID_DIR = "/run/moltnet/services";
|
|
114
|
+
|
|
65
115
|
/**
|
|
66
116
|
* Memory-backed VFS mount used by the daemon to inject task context
|
|
67
117
|
* (#943 slice 1.5). This is a separate top-level mount because Gondolin
|
|
@@ -94,6 +144,39 @@ export declare class GuestEnvironmentBoundaryError extends Error {
|
|
|
94
144
|
constructor(refusedNames: readonly string[]);
|
|
95
145
|
}
|
|
96
146
|
|
|
147
|
+
/** Declarative, trusted-host guest projection (env, files, services). */
|
|
148
|
+
export declare interface GuestProjectionInput {
|
|
149
|
+
env?: Record<string, string>;
|
|
150
|
+
files?: {
|
|
151
|
+
path: string;
|
|
152
|
+
content: string | Uint8Array;
|
|
153
|
+
mode?: number;
|
|
154
|
+
}[];
|
|
155
|
+
services?: {
|
|
156
|
+
id: string;
|
|
157
|
+
command: readonly string[];
|
|
158
|
+
env?: Record<string, string>;
|
|
159
|
+
/** Guest path to wait for before the session starts. */
|
|
160
|
+
readiness?: {
|
|
161
|
+
path: string;
|
|
162
|
+
timeoutMs?: number;
|
|
163
|
+
required?: boolean;
|
|
164
|
+
};
|
|
165
|
+
}[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Lifecycle handle for projected guest services. */
|
|
169
|
+
export declare interface GuestServices {
|
|
170
|
+
stop(): Promise<void>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Host origins: hostnames the guest can reach whose responses are produced by
|
|
175
|
+
* host code inside the proxy (`onRequest` short-circuit). Nothing is listened
|
|
176
|
+
* on, nothing is forwarded; the sandbox knows only the origin → handler map.
|
|
177
|
+
*/
|
|
178
|
+
declare type HostOriginHandler = (request: Request) => Promise<Response>;
|
|
179
|
+
|
|
97
180
|
/**
|
|
98
181
|
* Check containment for already-resolved lexical or real paths.
|
|
99
182
|
*
|
|
@@ -109,11 +192,25 @@ export declare function loadCredentials(agentDir: string, mode?: GuestCredential
|
|
|
109
192
|
export declare interface ManagedVm {
|
|
110
193
|
vm: VM;
|
|
111
194
|
credentials: VmCredentials;
|
|
195
|
+
/** Host-only rotation and revocation handle. It cannot widen destinations. */
|
|
196
|
+
secretManager: BrokeredHttpSecretManager;
|
|
197
|
+
/** Projected guest services; `stop()` before closing the VM. Always present. */
|
|
198
|
+
services: GuestServices;
|
|
112
199
|
mountPath: string;
|
|
113
200
|
guestWorkspace: string;
|
|
114
201
|
agentDir: string;
|
|
115
202
|
}
|
|
116
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Validate value-free descriptors, host-local bindings, environment
|
|
206
|
+
* collisions, and destination coverage before Gondolin creates any VM.
|
|
207
|
+
*/
|
|
208
|
+
export declare function prepareBrokeredHttpSecrets(options: {
|
|
209
|
+
bindings?: readonly BrokeredHttpSecretBinding[];
|
|
210
|
+
allowedHosts: readonly string[];
|
|
211
|
+
occupiedGuestEnvNames?: readonly string[];
|
|
212
|
+
}): Record<string, SecretDefinition>;
|
|
213
|
+
|
|
117
214
|
export declare interface ProviderAuthSource {
|
|
118
215
|
/** Read the auth blob from the host, or return null when absent. */
|
|
119
216
|
load(): string | null;
|
|
@@ -318,6 +415,25 @@ export declare interface VmConfig {
|
|
|
318
415
|
* into the guest without storing secret values in the profile.
|
|
319
416
|
*/
|
|
320
417
|
forwardEnv?: string[];
|
|
418
|
+
/**
|
|
419
|
+
* Trusted-host, per-attempt HTTP secret bindings. These are deliberately
|
|
420
|
+
* outside SandboxConfig so remotely stored runtime profiles cannot carry
|
|
421
|
+
* raw values or select host secret-provider coordinates.
|
|
422
|
+
*/
|
|
423
|
+
brokeredSecrets?: readonly BrokeredHttpSecretBinding[];
|
|
424
|
+
/**
|
|
425
|
+
* Trusted-host origins answered in-process by the proxy. Keys are full
|
|
426
|
+
* origins (`https://name.moltnet.internal`). Like brokered secrets these are
|
|
427
|
+
* outside SandboxConfig: a remotely stored profile cannot register one.
|
|
428
|
+
*/
|
|
429
|
+
hostOrigins?: Record<string, HostOriginHandler>;
|
|
430
|
+
/**
|
|
431
|
+
* Trusted-host guest projection: env merged last (after broker
|
|
432
|
+
* placeholders), files written before the session starts, services run in
|
|
433
|
+
* the guest for the session's lifetime. Not subject to the profile env
|
|
434
|
+
* reserved-name guard — the runtime, not the profile, declares it.
|
|
435
|
+
*/
|
|
436
|
+
guestProjection?: GuestProjectionInput;
|
|
321
437
|
/** Structured credential-boundary diagnostics for daemon loggers. */
|
|
322
438
|
onDiagnostic?: (diagnostic: VmDiagnostic) => void;
|
|
323
439
|
/** Abort resume/setup work, closing any live VM owned by resumeVm. */
|
|
@@ -355,10 +471,17 @@ export declare interface VmCredentials {
|
|
|
355
471
|
}
|
|
356
472
|
|
|
357
473
|
export declare interface VmDiagnostic {
|
|
358
|
-
event: 'vm.credentials.mode' | 'vm.credentials.github_key_missing';
|
|
474
|
+
event: 'vm.credentials.mode' | 'vm.credentials.github_key_missing' | 'vm.http_secrets.bound' | 'vm.host_origins.bound' | 'vm.guest_projection.applied' | 'vm.guest_service.not_ready';
|
|
359
475
|
level: 'info' | 'warning';
|
|
360
476
|
message: string;
|
|
361
477
|
credentialMode: GuestCredentialMode;
|
|
478
|
+
/** Present only for the value-free broker summary event. */
|
|
479
|
+
brokeredSecretCount?: number;
|
|
480
|
+
/** Present only for the host-origins summary event. */
|
|
481
|
+
hostOriginCount?: number;
|
|
482
|
+
/** Present only for the guest-projection summary event. */
|
|
483
|
+
projectedFileCount?: number;
|
|
484
|
+
projectedServiceCount?: number;
|
|
362
485
|
}
|
|
363
486
|
|
|
364
487
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -100,6 +100,7 @@ var BASE_ALPINE_PACKAGE_EXECUTABLES = {
|
|
|
100
100
|
file: "file",
|
|
101
101
|
git: "git",
|
|
102
102
|
jq: "jq",
|
|
103
|
+
"openssh-keygen": "ssh-keygen",
|
|
103
104
|
ripgrep: "rg",
|
|
104
105
|
tar: "tar",
|
|
105
106
|
xz: "xz"
|
|
@@ -292,6 +293,35 @@ function pruneOldSnapshots(maxCached, currentDir) {
|
|
|
292
293
|
});
|
|
293
294
|
}
|
|
294
295
|
//#endregion
|
|
296
|
+
//#region src/host-origins.ts
|
|
297
|
+
function hostOriginHostnames(origins) {
|
|
298
|
+
return Object.keys(origins).map((origin) => new URL(origin).hostname).sort();
|
|
299
|
+
}
|
|
300
|
+
function blocked() {
|
|
301
|
+
return new Response(JSON.stringify({
|
|
302
|
+
code: "host_origin_blocked",
|
|
303
|
+
message: "origin not served"
|
|
304
|
+
}), {
|
|
305
|
+
status: 421,
|
|
306
|
+
headers: { "content-type": "application/json" }
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Serve a registered origin in-process. Fails closed for any request whose
|
|
311
|
+
* hostname belongs to a virtual origin but whose exact origin (scheme, host,
|
|
312
|
+
* port) is not registered: such a request must never fall through to normal
|
|
313
|
+
* outbound proxy forwarding (an SSRF route to a host-controlled name).
|
|
314
|
+
*/
|
|
315
|
+
function createHostOriginsOnRequest(origins) {
|
|
316
|
+
const virtualHostnames = new Set(hostOriginHostnames(origins));
|
|
317
|
+
return async (request) => {
|
|
318
|
+
const url = new URL(request.url);
|
|
319
|
+
const handler = origins[url.origin];
|
|
320
|
+
if (handler) return handler(request);
|
|
321
|
+
if (virtualHostnames.has(url.hostname)) return blocked();
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
295
325
|
//#region src/vm-manager.ts
|
|
296
326
|
/**
|
|
297
327
|
* Memory-backed VFS mount used by the daemon to inject task context
|
|
@@ -314,6 +344,12 @@ function pruneOldSnapshots(maxCached, currentDir) {
|
|
|
314
344
|
* investigation and the alternatives we rejected.
|
|
315
345
|
*/
|
|
316
346
|
var GUEST_TASK_CONTEXT_MOUNT = "/moltnet-task-context";
|
|
347
|
+
/** Guest directory holding one PID file per projected service. */
|
|
348
|
+
var GUEST_SERVICE_PID_DIR = "/run/moltnet/services";
|
|
349
|
+
var GUEST_SERVICE_ID_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
350
|
+
function assertGuestServiceId(id) {
|
|
351
|
+
if (!GUEST_SERVICE_ID_RE.test(id)) throw new Error(`Invalid guest service id "${id}": expected ${GUEST_SERVICE_ID_RE}`);
|
|
352
|
+
}
|
|
317
353
|
/** @deprecated Use GUEST_TASK_CONTEXT_MOUNT. */
|
|
318
354
|
var GUEST_TASK_SKILLS_MOUNT = GUEST_TASK_CONTEXT_MOUNT;
|
|
319
355
|
function resolveVfsShadowConfig(config) {
|
|
@@ -529,6 +565,174 @@ var GuestEnvironmentBoundaryError = class extends Error {
|
|
|
529
565
|
this.name = "GuestEnvironmentBoundaryError";
|
|
530
566
|
}
|
|
531
567
|
};
|
|
568
|
+
var BrokeredHttpSecretBoundaryError = class extends Error {
|
|
569
|
+
constructor(issues) {
|
|
570
|
+
super("Brokered HTTP secret boundary refused the resolved bindings: " + issues.join("; "));
|
|
571
|
+
this.issues = issues;
|
|
572
|
+
this.name = "BrokeredHttpSecretBoundaryError";
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var BROKERED_SECRET_ID_REGEXP = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
576
|
+
var GUEST_ENV_NAME_REGEXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
577
|
+
var OBJECT_META_PROPERTY_NAMES = new Set([
|
|
578
|
+
"__proto__",
|
|
579
|
+
"constructor",
|
|
580
|
+
"prototype"
|
|
581
|
+
]);
|
|
582
|
+
function normalizeHostPattern(pattern) {
|
|
583
|
+
return pattern.trim().toLowerCase();
|
|
584
|
+
}
|
|
585
|
+
function isValidHostPattern(pattern) {
|
|
586
|
+
return pattern !== "" && !pattern.includes("://") && !pattern.includes("/") && !pattern.includes(":") && !/\s/.test(pattern);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Canonicalize and validate the value-free portion of a brokered binding.
|
|
590
|
+
* Runtime attestation and Gondolin enforcement both consume this function so
|
|
591
|
+
* the evidenced destination set cannot drift from the enforced one.
|
|
592
|
+
*/
|
|
593
|
+
function canonicalizeBrokeredHttpSecretDescriptor(descriptor) {
|
|
594
|
+
const issues = [];
|
|
595
|
+
const id = descriptor.id.trim();
|
|
596
|
+
const guestEnv = descriptor.guestEnv.trim();
|
|
597
|
+
const hosts = [...new Set(descriptor.hosts.map(normalizeHostPattern))].sort();
|
|
598
|
+
const protocolInput = descriptor.protocol ?? "https";
|
|
599
|
+
const protocol = protocolInput === "http" ? "http" : "https";
|
|
600
|
+
const ports = [...new Set(descriptor.ports ?? [protocol === "https" ? 443 : 80])].sort((left, right) => left - right);
|
|
601
|
+
if (!BROKERED_SECRET_ID_REGEXP.test(id)) issues.push(`invalid requirement id "${id || "<empty>"}"`);
|
|
602
|
+
if (!GUEST_ENV_NAME_REGEXP.test(guestEnv)) issues.push(`requirement "${id}" has invalid guest env "${guestEnv}"`);
|
|
603
|
+
else if (isReservedGuestEnvironmentName(guestEnv) || OBJECT_META_PROPERTY_NAMES.has(guestEnv)) issues.push(`requirement "${id}" uses reserved guest env "${guestEnv}"`);
|
|
604
|
+
if (hosts.length === 0) issues.push(`requirement "${id}" has no destination hosts`);
|
|
605
|
+
for (const host of hosts) if (!isValidHostPattern(host)) issues.push(`requirement "${id}" has invalid host pattern "${host}"`);
|
|
606
|
+
if (protocolInput !== "https" && protocolInput !== "http") issues.push(`requirement "${id}" has invalid protocol "${String(protocolInput)}"`);
|
|
607
|
+
if (ports.length === 0) issues.push(`requirement "${id}" has no destination ports`);
|
|
608
|
+
for (const port of ports) if (!Number.isInteger(port) || port < 1 || port > 65535) issues.push(`requirement "${id}" has invalid port "${port}"`);
|
|
609
|
+
if (issues.length > 0) throw new BrokeredHttpSecretBoundaryError(issues);
|
|
610
|
+
return Object.freeze({
|
|
611
|
+
id,
|
|
612
|
+
guestEnv,
|
|
613
|
+
hosts: Object.freeze(hosts),
|
|
614
|
+
protocol,
|
|
615
|
+
ports: Object.freeze(ports),
|
|
616
|
+
required: descriptor.required !== false
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function hostMatchesPattern(hostname, pattern) {
|
|
620
|
+
const expression = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
621
|
+
return new RegExp(`^${expression}$`, "i").test(hostname);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Conservative subset check for Gondolin hostname globs. Exact secret hosts
|
|
625
|
+
* may sit below a broader network wildcard. A wildcard secret destination must
|
|
626
|
+
* be repeated exactly (or covered by `*`) so a credential grant cannot widen
|
|
627
|
+
* through an ambiguous glob comparison.
|
|
628
|
+
*/
|
|
629
|
+
function networkPatternCoversSecretPattern(networkPattern, secretPattern) {
|
|
630
|
+
if (networkPattern === "*") return true;
|
|
631
|
+
if (secretPattern.includes("*")) return networkPattern === secretPattern;
|
|
632
|
+
return hostMatchesPattern(secretPattern, networkPattern);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Validate value-free descriptors, host-local bindings, environment
|
|
636
|
+
* collisions, and destination coverage before Gondolin creates any VM.
|
|
637
|
+
*/
|
|
638
|
+
function prepareBrokeredHttpSecrets(options) {
|
|
639
|
+
const issues = [];
|
|
640
|
+
const ids = /* @__PURE__ */ new Set();
|
|
641
|
+
const guestEnvNames = /* @__PURE__ */ new Set();
|
|
642
|
+
const occupiedGuestEnvNames = new Set(options.occupiedGuestEnvNames ?? []);
|
|
643
|
+
const allowedHosts = [...new Set(options.allowedHosts.map(normalizeHostPattern))];
|
|
644
|
+
const secrets = Object.create(null);
|
|
645
|
+
for (const binding of options.bindings ?? []) {
|
|
646
|
+
let descriptor;
|
|
647
|
+
try {
|
|
648
|
+
descriptor = canonicalizeBrokeredHttpSecretDescriptor(binding);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
if (error instanceof BrokeredHttpSecretBoundaryError) {
|
|
651
|
+
issues.push(...error.issues);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
const { id, guestEnv, hosts } = descriptor;
|
|
657
|
+
if (ids.has(id)) issues.push(`duplicate requirement id "${id}"`);
|
|
658
|
+
ids.add(id);
|
|
659
|
+
if (guestEnvNames.has(guestEnv)) issues.push(`duplicate guest env "${guestEnv}"`);
|
|
660
|
+
else if (occupiedGuestEnvNames.has(guestEnv)) issues.push(`guest env "${guestEnv}" is already supplied by another source`);
|
|
661
|
+
guestEnvNames.add(guestEnv);
|
|
662
|
+
for (const host of hosts) if (!allowedHosts.some((allowedHost) => networkPatternCoversSecretPattern(allowedHost, host))) issues.push(`requirement "${id}" host "${host}" is outside the effective network policy`);
|
|
663
|
+
const value = binding.value;
|
|
664
|
+
if (value === void 0 || value === "") {
|
|
665
|
+
if (descriptor.required) issues.push(`required binding "${id}" has no resolved value`);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
secrets[guestEnv] = {
|
|
669
|
+
hosts: [...hosts],
|
|
670
|
+
value
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (issues.length > 0) throw new BrokeredHttpSecretBoundaryError(issues);
|
|
674
|
+
return secrets;
|
|
675
|
+
}
|
|
676
|
+
function decodeBasicAuthorization(value) {
|
|
677
|
+
const match = /^Basic\s+([^\s]+)$/i.exec(value);
|
|
678
|
+
if (!match) return void 0;
|
|
679
|
+
try {
|
|
680
|
+
return Buffer.from(match[1], "base64").toString("utf8");
|
|
681
|
+
} catch {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function headersContainSecretValue(headers, value) {
|
|
686
|
+
if (value === "") return false;
|
|
687
|
+
for (const [name, headerValue] of headers.entries()) {
|
|
688
|
+
if (headerValue.includes(value)) return true;
|
|
689
|
+
if (/^(authorization|proxy-authorization)$/i.test(name) && decodeBasicAuthorization(headerValue)?.includes(value)) return true;
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
function createBrokeredHttpSecretOriginPolicy(bindings) {
|
|
694
|
+
const entries = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const binding of bindings) {
|
|
696
|
+
if (binding.value === void 0 || binding.value === "") continue;
|
|
697
|
+
const descriptor = canonicalizeBrokeredHttpSecretDescriptor(binding);
|
|
698
|
+
entries.set(descriptor.guestEnv, {
|
|
699
|
+
guestEnv: descriptor.guestEnv,
|
|
700
|
+
hosts: descriptor.hosts,
|
|
701
|
+
protocol: descriptor.protocol,
|
|
702
|
+
ports: descriptor.ports,
|
|
703
|
+
value: binding.value,
|
|
704
|
+
deleted: false
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
isRequestAllowed(request) {
|
|
709
|
+
let url;
|
|
710
|
+
try {
|
|
711
|
+
url = new URL(request.url);
|
|
712
|
+
} catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
const protocol = url.protocol.slice(0, -1);
|
|
716
|
+
const port = url.port ? Number(url.port) : protocol === "https" ? 443 : 80;
|
|
717
|
+
for (const entry of entries.values()) {
|
|
718
|
+
if (entry.deleted || !headersContainSecretValue(request.headers, entry.value)) continue;
|
|
719
|
+
if (protocol !== entry.protocol || !entry.ports.includes(port) || !entry.hosts.some((host) => hostMatchesPattern(url.hostname, host))) return false;
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
},
|
|
723
|
+
rotateSecret(guestEnv, value) {
|
|
724
|
+
const entry = entries.get(guestEnv);
|
|
725
|
+
if (!entry) throw new Error(`unknown brokered secret: ${guestEnv}`);
|
|
726
|
+
if (entry.deleted) throw new Error(`brokered secret revoked: ${guestEnv}`);
|
|
727
|
+
entry.value = value;
|
|
728
|
+
},
|
|
729
|
+
revokeSecret(guestEnv) {
|
|
730
|
+
const entry = entries.get(guestEnv);
|
|
731
|
+
if (!entry) throw new Error(`unknown brokered secret: ${guestEnv}`);
|
|
732
|
+
entry.deleted = true;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
532
736
|
function assertGuestEnvironmentBoundary(options) {
|
|
533
737
|
const refusedForwardEnv = (options.forwardEnv ?? []).filter((name) => isReservedGuestEnvironmentName(name) || options.guestCredentialMode === "host-authenticated" && !HOST_AUTHENTICATED_GUEST_ENV_ALLOWLIST.has(name));
|
|
534
738
|
const refusedSandboxEnv = Object.keys(options.sandboxEnv ?? {}).filter(isReservedGuestEnvironmentName);
|
|
@@ -584,6 +788,10 @@ function assertInternalHostsDoNotOverlapProtectedHosts(internalHosts, protectedH
|
|
|
584
788
|
* surface immediately rather than fall through to cryptic agent
|
|
585
789
|
* errors later.
|
|
586
790
|
*/
|
|
791
|
+
/** Single-quote a POSIX shell argument (paths/ids are pre-validated). */
|
|
792
|
+
function shellQuote(value) {
|
|
793
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
794
|
+
}
|
|
587
795
|
async function vmRun(vm, label, command, signal) {
|
|
588
796
|
const wrapped = `set -eu\nset -o pipefail\n${command}`;
|
|
589
797
|
throwIfAborted(signal, `resume step "${label}"`);
|
|
@@ -637,9 +845,61 @@ async function resumeVm(config) {
|
|
|
637
845
|
...config.extraAllowedHosts ?? []
|
|
638
846
|
])];
|
|
639
847
|
assertInternalHostsDoNotOverlapProtectedHosts(runtimeAllowedInternalHosts, protectedExternalHosts);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
848
|
+
const allowedHosts = [...new Set([...protectedExternalHosts, ...runtimeAllowedHosts])];
|
|
849
|
+
const hostOrigins = config.hostOrigins ?? {};
|
|
850
|
+
const hostOriginHosts = hostOriginHostnames(hostOrigins);
|
|
851
|
+
assertInternalHostsDoNotOverlapProtectedHosts(hostOriginHosts, protectedExternalHosts);
|
|
852
|
+
const allowedInternalHosts = [...new Set([...runtimeAllowedInternalHosts, ...hostOriginHosts])];
|
|
853
|
+
const projectedEnv = config.guestProjection?.env ?? {};
|
|
854
|
+
const brokeredSecrets = prepareBrokeredHttpSecrets({
|
|
855
|
+
bindings: config.brokeredSecrets,
|
|
856
|
+
allowedHosts: [...allowedHosts, ...allowedInternalHosts],
|
|
857
|
+
occupiedGuestEnvNames: [
|
|
858
|
+
"PATH",
|
|
859
|
+
"HOME",
|
|
860
|
+
"NODE_NO_WARNINGS",
|
|
861
|
+
"NODE_EXTRA_CA_CERTS",
|
|
862
|
+
"MOLTNET_GUEST_WORKSPACE",
|
|
863
|
+
...config.forwardEnv ?? [],
|
|
864
|
+
...Object.keys(creds.agentEnv),
|
|
865
|
+
...Object.keys(config.sandboxConfig?.env ?? {}),
|
|
866
|
+
...Object.keys(projectedEnv)
|
|
867
|
+
]
|
|
868
|
+
});
|
|
869
|
+
const brokeredSecretOriginPolicy = createBrokeredHttpSecretOriginPolicy(config.brokeredSecrets ?? []);
|
|
870
|
+
const { httpHooks, env: secretEnv, secretManager: gondolinSecretManager } = createHttpHooks({
|
|
871
|
+
allowedHosts,
|
|
872
|
+
allowedInternalHosts,
|
|
873
|
+
...Object.keys(brokeredSecrets).length > 0 && {
|
|
874
|
+
secrets: brokeredSecrets,
|
|
875
|
+
isRequestAllowed: brokeredSecretOriginPolicy.isRequestAllowed
|
|
876
|
+
},
|
|
877
|
+
...hostOriginHosts.length > 0 && { onRequest: createHostOriginsOnRequest(hostOrigins) }
|
|
878
|
+
});
|
|
879
|
+
const secretManager = Object.freeze({
|
|
880
|
+
rotateSecret(guestEnv, value) {
|
|
881
|
+
gondolinSecretManager.updateSecret(guestEnv, { value });
|
|
882
|
+
brokeredSecretOriginPolicy.rotateSecret(guestEnv, value);
|
|
883
|
+
},
|
|
884
|
+
revokeSecret(guestEnv) {
|
|
885
|
+
gondolinSecretManager.deleteSecret(guestEnv);
|
|
886
|
+
brokeredSecretOriginPolicy.revokeSecret(guestEnv);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
const brokeredSecretCount = Object.keys(brokeredSecrets).length;
|
|
890
|
+
if (brokeredSecretCount > 0) config.onDiagnostic?.({
|
|
891
|
+
event: "vm.http_secrets.bound",
|
|
892
|
+
level: "info",
|
|
893
|
+
credentialMode: guestCredentialMode,
|
|
894
|
+
brokeredSecretCount,
|
|
895
|
+
message: `Bound ${brokeredSecretCount} HTTP secret placeholder${brokeredSecretCount === 1 ? "" : "s"} to the host proxy`
|
|
896
|
+
});
|
|
897
|
+
if (hostOriginHosts.length > 0) config.onDiagnostic?.({
|
|
898
|
+
event: "vm.host_origins.bound",
|
|
899
|
+
level: "info",
|
|
900
|
+
credentialMode: guestCredentialMode,
|
|
901
|
+
hostOriginCount: hostOriginHosts.length,
|
|
902
|
+
message: `Bound ${hostOriginHosts.length} host origin${hostOriginHosts.length === 1 ? "" : "s"} to the host proxy`
|
|
643
903
|
});
|
|
644
904
|
const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
|
|
645
905
|
const vmAgentEnv = {};
|
|
@@ -678,7 +938,6 @@ async function resumeVm(config) {
|
|
|
678
938
|
}
|
|
679
939
|
const envOverrides = config.sandboxConfig?.env ?? {};
|
|
680
940
|
const vmEnv = {
|
|
681
|
-
...secretEnv,
|
|
682
941
|
...vmAgentEnv,
|
|
683
942
|
...forwardedEnv,
|
|
684
943
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/go/bin",
|
|
@@ -686,7 +945,9 @@ async function resumeVm(config) {
|
|
|
686
945
|
NODE_NO_WARNINGS: "1",
|
|
687
946
|
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
|
|
688
947
|
...envOverrides,
|
|
689
|
-
MOLTNET_GUEST_WORKSPACE: guestWorkspace
|
|
948
|
+
MOLTNET_GUEST_WORKSPACE: guestWorkspace,
|
|
949
|
+
...secretEnv,
|
|
950
|
+
...projectedEnv
|
|
690
951
|
};
|
|
691
952
|
const resources = config.sandboxConfig?.resources;
|
|
692
953
|
const workspaceMode = config.workspaceMode ?? "shared_mount";
|
|
@@ -709,6 +970,27 @@ async function resumeVm(config) {
|
|
|
709
970
|
process.stderr.write(`[vm] aborted resume late vm.close() failed: ${message}\n`);
|
|
710
971
|
}
|
|
711
972
|
});
|
|
973
|
+
const servicesAbort = new AbortController();
|
|
974
|
+
const serviceHandles = [];
|
|
975
|
+
const serviceIds = [];
|
|
976
|
+
const services = { async stop() {
|
|
977
|
+
if (serviceIds.length > 0) {
|
|
978
|
+
const pidFiles = serviceIds.map((id) => `${GUEST_SERVICE_PID_DIR}/${id}.pid`).join(" ");
|
|
979
|
+
try {
|
|
980
|
+
await vm.exec([
|
|
981
|
+
"sh",
|
|
982
|
+
"-c",
|
|
983
|
+
`for f in ${pidFiles}; do [ -f "$f" ] || continue; pid=$(cat "$f"); kill -TERM -- "-\$pid" 2>/dev/null || kill -TERM "\$pid" 2>/dev/null || true; done; sleep 0.2; for f in ${pidFiles}; do [ -f "$f" ] || continue; pid=$(cat "$f"); kill -KILL -- "-\$pid" 2>/dev/null || kill -KILL "\$pid" 2>/dev/null || true; rm -f "\$f"; done`
|
|
984
|
+
], {
|
|
985
|
+
stdout: "ignore",
|
|
986
|
+
stderr: "ignore"
|
|
987
|
+
});
|
|
988
|
+
} catch {}
|
|
989
|
+
serviceIds.length = 0;
|
|
990
|
+
}
|
|
991
|
+
servicesAbort.abort();
|
|
992
|
+
await Promise.allSettled(serviceHandles);
|
|
993
|
+
} };
|
|
712
994
|
try {
|
|
713
995
|
await vmRun(vm, "TLS certificates", `
|
|
714
996
|
cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
|
|
@@ -744,8 +1026,14 @@ async function resumeVm(config) {
|
|
|
744
1026
|
const vmSshDir = `${vmAgentDir}/ssh`;
|
|
745
1027
|
const hasAgentFiles = guestCredentialMode === "guest-config";
|
|
746
1028
|
const providerAuthDir = config.providerAuth ? path.posix.dirname(config.providerAuth.guestPath) : null;
|
|
747
|
-
const
|
|
748
|
-
|
|
1029
|
+
const projectedFiles = config.guestProjection?.files ?? [];
|
|
1030
|
+
const projectedDirs = [...new Set(projectedFiles.map((file) => path.posix.dirname(file.path)))];
|
|
1031
|
+
const guestDirs = [
|
|
1032
|
+
...hasAgentFiles ? [`${vmAgentDir}/ssh`] : [],
|
|
1033
|
+
...providerAuthDir ? [providerAuthDir] : [],
|
|
1034
|
+
...projectedDirs
|
|
1035
|
+
];
|
|
1036
|
+
if (guestDirs.length > 0) await vmRun(vm, "create guest directories", `mkdir -p ${guestDirs.map(shellQuote).join(" ")}`, config.signal);
|
|
749
1037
|
if (creds.providerAuthJson !== null && config.providerAuth) await vm.fs.writeFile(config.providerAuth.guestPath, creds.providerAuthJson, {
|
|
750
1038
|
mode: 384,
|
|
751
1039
|
signal: config.signal
|
|
@@ -784,15 +1072,94 @@ async function resumeVm(config) {
|
|
|
784
1072
|
signal: config.signal
|
|
785
1073
|
});
|
|
786
1074
|
}
|
|
787
|
-
|
|
1075
|
+
for (const file of projectedFiles) {
|
|
1076
|
+
await vm.fs.writeFile(file.path, file.content, {
|
|
1077
|
+
mode: file.mode ?? 420,
|
|
1078
|
+
signal: config.signal
|
|
1079
|
+
});
|
|
1080
|
+
if (file.mode !== void 0) await vmRun(vm, `chmod projected file ${file.path}`, `chmod ${file.mode.toString(8)} ${shellQuote(file.path)}`, config.signal);
|
|
1081
|
+
}
|
|
1082
|
+
await vmRun(vm, "chown guest directories", `set -e; for d in ${[
|
|
1083
|
+
"/home/agent/.pi",
|
|
1084
|
+
...hasAgentFiles ? ["/home/agent/.moltnet"] : [],
|
|
1085
|
+
...projectedDirs.filter((dir) => dir.startsWith("/home/agent/"))
|
|
1086
|
+
].map(shellQuote).join(" ")}; do if [ -e "$d" ]; then chown -R agent:agent "$d"; fi; done`, config.signal);
|
|
1087
|
+
const projectedServices = config.guestProjection?.services ?? [];
|
|
1088
|
+
if (projectedServices.length > 0) await vmRun(vm, "create service pid directory", `mkdir -p ${GUEST_SERVICE_PID_DIR}`, config.signal);
|
|
1089
|
+
for (const service of projectedServices) {
|
|
1090
|
+
assertGuestServiceId(service.id);
|
|
1091
|
+
const handle = vm.exec([
|
|
1092
|
+
"setsid",
|
|
1093
|
+
"sh",
|
|
1094
|
+
"-c",
|
|
1095
|
+
`echo $$ > ${GUEST_SERVICE_PID_DIR}/${service.id}.pid && exec "$@"`,
|
|
1096
|
+
"moltnet-service",
|
|
1097
|
+
...service.command
|
|
1098
|
+
], {
|
|
1099
|
+
stdout: "ignore",
|
|
1100
|
+
stderr: "ignore",
|
|
1101
|
+
...service.env && { env: service.env },
|
|
1102
|
+
signal: servicesAbort.signal
|
|
1103
|
+
});
|
|
1104
|
+
serviceHandles.push(Promise.resolve(handle).catch(() => void 0));
|
|
1105
|
+
serviceIds.push(service.id);
|
|
1106
|
+
}
|
|
1107
|
+
async function awaitServiceReady(service) {
|
|
1108
|
+
if (!service.readiness) return;
|
|
1109
|
+
const deadline = Date.now() + (service.readiness.timeoutMs ?? 1e4);
|
|
1110
|
+
const pidFile = `${GUEST_SERVICE_PID_DIR}/${service.id}.pid`;
|
|
1111
|
+
let ready = false;
|
|
1112
|
+
while (Date.now() < deadline) {
|
|
1113
|
+
throwIfAborted(config.signal, `service "${service.id}" readiness`);
|
|
1114
|
+
if ((await vm.exec([
|
|
1115
|
+
"sh",
|
|
1116
|
+
"-c",
|
|
1117
|
+
"[ -e \"$1\" ] && [ -r \"$2\" ] && kill -0 \"$(cat \"$2\" 2>/dev/null)\" 2>/dev/null",
|
|
1118
|
+
"moltnet-readiness",
|
|
1119
|
+
service.readiness.path,
|
|
1120
|
+
pidFile
|
|
1121
|
+
], {
|
|
1122
|
+
stdout: "ignore",
|
|
1123
|
+
stderr: "ignore",
|
|
1124
|
+
signal: config.signal
|
|
1125
|
+
})).exitCode === 0) {
|
|
1126
|
+
ready = true;
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
await new Promise((resolve) => {
|
|
1130
|
+
setTimeout(resolve, 200);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
if (!ready) {
|
|
1134
|
+
if (service.readiness.required) throw new Error(`Projected guest service "${service.id}" did not become ready: ${service.readiness.path} absent or its process exited`);
|
|
1135
|
+
config.onDiagnostic?.({
|
|
1136
|
+
event: "vm.guest_service.not_ready",
|
|
1137
|
+
level: "warning",
|
|
1138
|
+
credentialMode: guestCredentialMode,
|
|
1139
|
+
message: `Projected guest service "${service.id}" did not become ready (${service.readiness.path} absent or its process exited); continuing without it`
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
await Promise.all(projectedServices.map((service) => awaitServiceReady(service)));
|
|
1144
|
+
if (projectedFiles.length > 0 || projectedServices.length > 0) config.onDiagnostic?.({
|
|
1145
|
+
event: "vm.guest_projection.applied",
|
|
1146
|
+
level: "info",
|
|
1147
|
+
credentialMode: guestCredentialMode,
|
|
1148
|
+
projectedFileCount: projectedFiles.length,
|
|
1149
|
+
projectedServiceCount: projectedServices.length,
|
|
1150
|
+
message: `Applied guest projection: ${projectedFiles.length} file(s), ${projectedServices.length} service(s)`
|
|
1151
|
+
});
|
|
788
1152
|
return {
|
|
789
1153
|
vm,
|
|
790
1154
|
credentials: creds,
|
|
1155
|
+
secretManager,
|
|
1156
|
+
services,
|
|
791
1157
|
mountPath: config.mountPath,
|
|
792
1158
|
guestWorkspace,
|
|
793
1159
|
agentDir
|
|
794
1160
|
};
|
|
795
1161
|
} catch (err) {
|
|
1162
|
+
servicesAbort.abort();
|
|
796
1163
|
try {
|
|
797
1164
|
await vm.close();
|
|
798
1165
|
} catch (closeErr) {
|
|
@@ -864,4 +1231,4 @@ function rewriteMoltnetJsonPaths(moltnetJson, vmAgentDir, vmSshDir, githubAppPem
|
|
|
864
1231
|
return JSON.stringify(config);
|
|
865
1232
|
}
|
|
866
1233
|
//#endregion
|
|
867
|
-
export { AutoParentMemoryProvider, GONDOLIN_BASE_EXECUTABLES, GUEST_TASK_CONTEXT_MOUNT, GUEST_TASK_SKILLS_MOUNT, GuestEnvironmentBoundaryError, abortableResource, activateAgentEnv, assertGuestEnvironmentBoundary, assertHostAuthenticatedGuestEnvironment, delay, ensureSnapshot, findMainWorktree, isResolvedPathInsideRoot, loadCredentials, resolveVfsShadowConfig, resolveVmAgentDir, resumeVm, rewriteGitconfigPaths, rewriteMoltnetJsonPaths, shouldRunResumeCommand, shouldShadowNodeModulesPath, throwIfAborted };
|
|
1234
|
+
export { AutoParentMemoryProvider, BrokeredHttpSecretBoundaryError, GONDOLIN_BASE_EXECUTABLES, GUEST_SERVICE_PID_DIR, GUEST_TASK_CONTEXT_MOUNT, GUEST_TASK_SKILLS_MOUNT, GuestEnvironmentBoundaryError, abortableResource, activateAgentEnv, assertGuestEnvironmentBoundary, assertHostAuthenticatedGuestEnvironment, canonicalizeBrokeredHttpSecretDescriptor, delay, ensureSnapshot, findMainWorktree, isResolvedPathInsideRoot, loadCredentials, prepareBrokeredHttpSecrets, resolveVfsShadowConfig, resolveVmAgentDir, resumeVm, rewriteGitconfigPaths, rewriteMoltnetJsonPaths, shouldRunResumeCommand, shouldShadowNodeModulesPath, throwIfAborted };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/sandbox-gondolin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Gondolin microVM sandbox lifecycle for MoltNet runtimes: checkpoint resume, egress policy, VFS shadowing, guest credential boundary, host-brokered secrets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moltnet",
|