@themoltnet/sandbox-gondolin 0.1.0 → 0.2.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 +68 -1
- package/dist/index.js +213 -6
- 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
|
/**
|
|
@@ -109,11 +156,23 @@ export declare function loadCredentials(agentDir: string, mode?: GuestCredential
|
|
|
109
156
|
export declare interface ManagedVm {
|
|
110
157
|
vm: VM;
|
|
111
158
|
credentials: VmCredentials;
|
|
159
|
+
/** Host-only rotation and revocation handle. It cannot widen destinations. */
|
|
160
|
+
secretManager: BrokeredHttpSecretManager;
|
|
112
161
|
mountPath: string;
|
|
113
162
|
guestWorkspace: string;
|
|
114
163
|
agentDir: string;
|
|
115
164
|
}
|
|
116
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Validate value-free descriptors, host-local bindings, environment
|
|
168
|
+
* collisions, and destination coverage before Gondolin creates any VM.
|
|
169
|
+
*/
|
|
170
|
+
export declare function prepareBrokeredHttpSecrets(options: {
|
|
171
|
+
bindings?: readonly BrokeredHttpSecretBinding[];
|
|
172
|
+
allowedHosts: readonly string[];
|
|
173
|
+
occupiedGuestEnvNames?: readonly string[];
|
|
174
|
+
}): Record<string, SecretDefinition>;
|
|
175
|
+
|
|
117
176
|
export declare interface ProviderAuthSource {
|
|
118
177
|
/** Read the auth blob from the host, or return null when absent. */
|
|
119
178
|
load(): string | null;
|
|
@@ -318,6 +377,12 @@ export declare interface VmConfig {
|
|
|
318
377
|
* into the guest without storing secret values in the profile.
|
|
319
378
|
*/
|
|
320
379
|
forwardEnv?: string[];
|
|
380
|
+
/**
|
|
381
|
+
* Trusted-host, per-attempt HTTP secret bindings. These are deliberately
|
|
382
|
+
* outside SandboxConfig so remotely stored runtime profiles cannot carry
|
|
383
|
+
* raw values or select host secret-provider coordinates.
|
|
384
|
+
*/
|
|
385
|
+
brokeredSecrets?: readonly BrokeredHttpSecretBinding[];
|
|
321
386
|
/** Structured credential-boundary diagnostics for daemon loggers. */
|
|
322
387
|
onDiagnostic?: (diagnostic: VmDiagnostic) => void;
|
|
323
388
|
/** Abort resume/setup work, closing any live VM owned by resumeVm. */
|
|
@@ -355,10 +420,12 @@ export declare interface VmCredentials {
|
|
|
355
420
|
}
|
|
356
421
|
|
|
357
422
|
export declare interface VmDiagnostic {
|
|
358
|
-
event: 'vm.credentials.mode' | 'vm.credentials.github_key_missing';
|
|
423
|
+
event: 'vm.credentials.mode' | 'vm.credentials.github_key_missing' | 'vm.http_secrets.bound';
|
|
359
424
|
level: 'info' | 'warning';
|
|
360
425
|
message: string;
|
|
361
426
|
credentialMode: GuestCredentialMode;
|
|
427
|
+
/** Present only for the value-free broker summary event. */
|
|
428
|
+
brokeredSecretCount?: number;
|
|
362
429
|
}
|
|
363
430
|
|
|
364
431
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -529,6 +529,174 @@ var GuestEnvironmentBoundaryError = class extends Error {
|
|
|
529
529
|
this.name = "GuestEnvironmentBoundaryError";
|
|
530
530
|
}
|
|
531
531
|
};
|
|
532
|
+
var BrokeredHttpSecretBoundaryError = class extends Error {
|
|
533
|
+
constructor(issues) {
|
|
534
|
+
super("Brokered HTTP secret boundary refused the resolved bindings: " + issues.join("; "));
|
|
535
|
+
this.issues = issues;
|
|
536
|
+
this.name = "BrokeredHttpSecretBoundaryError";
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var BROKERED_SECRET_ID_REGEXP = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
540
|
+
var GUEST_ENV_NAME_REGEXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
541
|
+
var OBJECT_META_PROPERTY_NAMES = new Set([
|
|
542
|
+
"__proto__",
|
|
543
|
+
"constructor",
|
|
544
|
+
"prototype"
|
|
545
|
+
]);
|
|
546
|
+
function normalizeHostPattern(pattern) {
|
|
547
|
+
return pattern.trim().toLowerCase();
|
|
548
|
+
}
|
|
549
|
+
function isValidHostPattern(pattern) {
|
|
550
|
+
return pattern !== "" && !pattern.includes("://") && !pattern.includes("/") && !pattern.includes(":") && !/\s/.test(pattern);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Canonicalize and validate the value-free portion of a brokered binding.
|
|
554
|
+
* Runtime attestation and Gondolin enforcement both consume this function so
|
|
555
|
+
* the evidenced destination set cannot drift from the enforced one.
|
|
556
|
+
*/
|
|
557
|
+
function canonicalizeBrokeredHttpSecretDescriptor(descriptor) {
|
|
558
|
+
const issues = [];
|
|
559
|
+
const id = descriptor.id.trim();
|
|
560
|
+
const guestEnv = descriptor.guestEnv.trim();
|
|
561
|
+
const hosts = [...new Set(descriptor.hosts.map(normalizeHostPattern))].sort();
|
|
562
|
+
const protocolInput = descriptor.protocol ?? "https";
|
|
563
|
+
const protocol = protocolInput === "http" ? "http" : "https";
|
|
564
|
+
const ports = [...new Set(descriptor.ports ?? [protocol === "https" ? 443 : 80])].sort((left, right) => left - right);
|
|
565
|
+
if (!BROKERED_SECRET_ID_REGEXP.test(id)) issues.push(`invalid requirement id "${id || "<empty>"}"`);
|
|
566
|
+
if (!GUEST_ENV_NAME_REGEXP.test(guestEnv)) issues.push(`requirement "${id}" has invalid guest env "${guestEnv}"`);
|
|
567
|
+
else if (isReservedGuestEnvironmentName(guestEnv) || OBJECT_META_PROPERTY_NAMES.has(guestEnv)) issues.push(`requirement "${id}" uses reserved guest env "${guestEnv}"`);
|
|
568
|
+
if (hosts.length === 0) issues.push(`requirement "${id}" has no destination hosts`);
|
|
569
|
+
for (const host of hosts) if (!isValidHostPattern(host)) issues.push(`requirement "${id}" has invalid host pattern "${host}"`);
|
|
570
|
+
if (protocolInput !== "https" && protocolInput !== "http") issues.push(`requirement "${id}" has invalid protocol "${String(protocolInput)}"`);
|
|
571
|
+
if (ports.length === 0) issues.push(`requirement "${id}" has no destination ports`);
|
|
572
|
+
for (const port of ports) if (!Number.isInteger(port) || port < 1 || port > 65535) issues.push(`requirement "${id}" has invalid port "${port}"`);
|
|
573
|
+
if (issues.length > 0) throw new BrokeredHttpSecretBoundaryError(issues);
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
id,
|
|
576
|
+
guestEnv,
|
|
577
|
+
hosts: Object.freeze(hosts),
|
|
578
|
+
protocol,
|
|
579
|
+
ports: Object.freeze(ports),
|
|
580
|
+
required: descriptor.required !== false
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function hostMatchesPattern(hostname, pattern) {
|
|
584
|
+
const expression = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
585
|
+
return new RegExp(`^${expression}$`, "i").test(hostname);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Conservative subset check for Gondolin hostname globs. Exact secret hosts
|
|
589
|
+
* may sit below a broader network wildcard. A wildcard secret destination must
|
|
590
|
+
* be repeated exactly (or covered by `*`) so a credential grant cannot widen
|
|
591
|
+
* through an ambiguous glob comparison.
|
|
592
|
+
*/
|
|
593
|
+
function networkPatternCoversSecretPattern(networkPattern, secretPattern) {
|
|
594
|
+
if (networkPattern === "*") return true;
|
|
595
|
+
if (secretPattern.includes("*")) return networkPattern === secretPattern;
|
|
596
|
+
return hostMatchesPattern(secretPattern, networkPattern);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Validate value-free descriptors, host-local bindings, environment
|
|
600
|
+
* collisions, and destination coverage before Gondolin creates any VM.
|
|
601
|
+
*/
|
|
602
|
+
function prepareBrokeredHttpSecrets(options) {
|
|
603
|
+
const issues = [];
|
|
604
|
+
const ids = /* @__PURE__ */ new Set();
|
|
605
|
+
const guestEnvNames = /* @__PURE__ */ new Set();
|
|
606
|
+
const occupiedGuestEnvNames = new Set(options.occupiedGuestEnvNames ?? []);
|
|
607
|
+
const allowedHosts = [...new Set(options.allowedHosts.map(normalizeHostPattern))];
|
|
608
|
+
const secrets = Object.create(null);
|
|
609
|
+
for (const binding of options.bindings ?? []) {
|
|
610
|
+
let descriptor;
|
|
611
|
+
try {
|
|
612
|
+
descriptor = canonicalizeBrokeredHttpSecretDescriptor(binding);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof BrokeredHttpSecretBoundaryError) {
|
|
615
|
+
issues.push(...error.issues);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
const { id, guestEnv, hosts } = descriptor;
|
|
621
|
+
if (ids.has(id)) issues.push(`duplicate requirement id "${id}"`);
|
|
622
|
+
ids.add(id);
|
|
623
|
+
if (guestEnvNames.has(guestEnv)) issues.push(`duplicate guest env "${guestEnv}"`);
|
|
624
|
+
else if (occupiedGuestEnvNames.has(guestEnv)) issues.push(`guest env "${guestEnv}" is already supplied by another source`);
|
|
625
|
+
guestEnvNames.add(guestEnv);
|
|
626
|
+
for (const host of hosts) if (!allowedHosts.some((allowedHost) => networkPatternCoversSecretPattern(allowedHost, host))) issues.push(`requirement "${id}" host "${host}" is outside the effective network policy`);
|
|
627
|
+
const value = binding.value;
|
|
628
|
+
if (value === void 0 || value === "") {
|
|
629
|
+
if (descriptor.required) issues.push(`required binding "${id}" has no resolved value`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
secrets[guestEnv] = {
|
|
633
|
+
hosts: [...hosts],
|
|
634
|
+
value
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (issues.length > 0) throw new BrokeredHttpSecretBoundaryError(issues);
|
|
638
|
+
return secrets;
|
|
639
|
+
}
|
|
640
|
+
function decodeBasicAuthorization(value) {
|
|
641
|
+
const match = /^Basic\s+([^\s]+)$/i.exec(value);
|
|
642
|
+
if (!match) return void 0;
|
|
643
|
+
try {
|
|
644
|
+
return Buffer.from(match[1], "base64").toString("utf8");
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function headersContainSecretValue(headers, value) {
|
|
650
|
+
if (value === "") return false;
|
|
651
|
+
for (const [name, headerValue] of headers.entries()) {
|
|
652
|
+
if (headerValue.includes(value)) return true;
|
|
653
|
+
if (/^(authorization|proxy-authorization)$/i.test(name) && decodeBasicAuthorization(headerValue)?.includes(value)) return true;
|
|
654
|
+
}
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
function createBrokeredHttpSecretOriginPolicy(bindings) {
|
|
658
|
+
const entries = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const binding of bindings) {
|
|
660
|
+
if (binding.value === void 0 || binding.value === "") continue;
|
|
661
|
+
const descriptor = canonicalizeBrokeredHttpSecretDescriptor(binding);
|
|
662
|
+
entries.set(descriptor.guestEnv, {
|
|
663
|
+
guestEnv: descriptor.guestEnv,
|
|
664
|
+
hosts: descriptor.hosts,
|
|
665
|
+
protocol: descriptor.protocol,
|
|
666
|
+
ports: descriptor.ports,
|
|
667
|
+
value: binding.value,
|
|
668
|
+
deleted: false
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
isRequestAllowed(request) {
|
|
673
|
+
let url;
|
|
674
|
+
try {
|
|
675
|
+
url = new URL(request.url);
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const protocol = url.protocol.slice(0, -1);
|
|
680
|
+
const port = url.port ? Number(url.port) : protocol === "https" ? 443 : 80;
|
|
681
|
+
for (const entry of entries.values()) {
|
|
682
|
+
if (entry.deleted || !headersContainSecretValue(request.headers, entry.value)) continue;
|
|
683
|
+
if (protocol !== entry.protocol || !entry.ports.includes(port) || !entry.hosts.some((host) => hostMatchesPattern(url.hostname, host))) return false;
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
},
|
|
687
|
+
rotateSecret(guestEnv, value) {
|
|
688
|
+
const entry = entries.get(guestEnv);
|
|
689
|
+
if (!entry) throw new Error(`unknown brokered secret: ${guestEnv}`);
|
|
690
|
+
if (entry.deleted) throw new Error(`brokered secret revoked: ${guestEnv}`);
|
|
691
|
+
entry.value = value;
|
|
692
|
+
},
|
|
693
|
+
revokeSecret(guestEnv) {
|
|
694
|
+
const entry = entries.get(guestEnv);
|
|
695
|
+
if (!entry) throw new Error(`unknown brokered secret: ${guestEnv}`);
|
|
696
|
+
entry.deleted = true;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
532
700
|
function assertGuestEnvironmentBoundary(options) {
|
|
533
701
|
const refusedForwardEnv = (options.forwardEnv ?? []).filter((name) => isReservedGuestEnvironmentName(name) || options.guestCredentialMode === "host-authenticated" && !HOST_AUTHENTICATED_GUEST_ENV_ALLOWLIST.has(name));
|
|
534
702
|
const refusedSandboxEnv = Object.keys(options.sandboxEnv ?? {}).filter(isReservedGuestEnvironmentName);
|
|
@@ -637,9 +805,47 @@ async function resumeVm(config) {
|
|
|
637
805
|
...config.extraAllowedHosts ?? []
|
|
638
806
|
])];
|
|
639
807
|
assertInternalHostsDoNotOverlapProtectedHosts(runtimeAllowedInternalHosts, protectedExternalHosts);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
808
|
+
const allowedHosts = [...new Set([...protectedExternalHosts, ...runtimeAllowedHosts])];
|
|
809
|
+
const brokeredSecrets = prepareBrokeredHttpSecrets({
|
|
810
|
+
bindings: config.brokeredSecrets,
|
|
811
|
+
allowedHosts: [...allowedHosts, ...runtimeAllowedInternalHosts],
|
|
812
|
+
occupiedGuestEnvNames: [
|
|
813
|
+
"PATH",
|
|
814
|
+
"HOME",
|
|
815
|
+
"NODE_NO_WARNINGS",
|
|
816
|
+
"NODE_EXTRA_CA_CERTS",
|
|
817
|
+
"MOLTNET_GUEST_WORKSPACE",
|
|
818
|
+
...config.forwardEnv ?? [],
|
|
819
|
+
...Object.keys(creds.agentEnv),
|
|
820
|
+
...Object.keys(config.sandboxConfig?.env ?? {})
|
|
821
|
+
]
|
|
822
|
+
});
|
|
823
|
+
const brokeredSecretOriginPolicy = createBrokeredHttpSecretOriginPolicy(config.brokeredSecrets ?? []);
|
|
824
|
+
const { httpHooks, env: secretEnv, secretManager: gondolinSecretManager } = createHttpHooks({
|
|
825
|
+
allowedHosts,
|
|
826
|
+
allowedInternalHosts: runtimeAllowedInternalHosts,
|
|
827
|
+
...Object.keys(brokeredSecrets).length > 0 && {
|
|
828
|
+
secrets: brokeredSecrets,
|
|
829
|
+
isRequestAllowed: brokeredSecretOriginPolicy.isRequestAllowed
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
const secretManager = Object.freeze({
|
|
833
|
+
rotateSecret(guestEnv, value) {
|
|
834
|
+
gondolinSecretManager.updateSecret(guestEnv, { value });
|
|
835
|
+
brokeredSecretOriginPolicy.rotateSecret(guestEnv, value);
|
|
836
|
+
},
|
|
837
|
+
revokeSecret(guestEnv) {
|
|
838
|
+
gondolinSecretManager.deleteSecret(guestEnv);
|
|
839
|
+
brokeredSecretOriginPolicy.revokeSecret(guestEnv);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
const brokeredSecretCount = Object.keys(brokeredSecrets).length;
|
|
843
|
+
if (brokeredSecretCount > 0) config.onDiagnostic?.({
|
|
844
|
+
event: "vm.http_secrets.bound",
|
|
845
|
+
level: "info",
|
|
846
|
+
credentialMode: guestCredentialMode,
|
|
847
|
+
brokeredSecretCount,
|
|
848
|
+
message: `Bound ${brokeredSecretCount} HTTP secret placeholder${brokeredSecretCount === 1 ? "" : "s"} to the host proxy`
|
|
643
849
|
});
|
|
644
850
|
const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
|
|
645
851
|
const vmAgentEnv = {};
|
|
@@ -678,7 +884,6 @@ async function resumeVm(config) {
|
|
|
678
884
|
}
|
|
679
885
|
const envOverrides = config.sandboxConfig?.env ?? {};
|
|
680
886
|
const vmEnv = {
|
|
681
|
-
...secretEnv,
|
|
682
887
|
...vmAgentEnv,
|
|
683
888
|
...forwardedEnv,
|
|
684
889
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/go/bin",
|
|
@@ -686,7 +891,8 @@ async function resumeVm(config) {
|
|
|
686
891
|
NODE_NO_WARNINGS: "1",
|
|
687
892
|
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
|
|
688
893
|
...envOverrides,
|
|
689
|
-
MOLTNET_GUEST_WORKSPACE: guestWorkspace
|
|
894
|
+
MOLTNET_GUEST_WORKSPACE: guestWorkspace,
|
|
895
|
+
...secretEnv
|
|
690
896
|
};
|
|
691
897
|
const resources = config.sandboxConfig?.resources;
|
|
692
898
|
const workspaceMode = config.workspaceMode ?? "shared_mount";
|
|
@@ -788,6 +994,7 @@ async function resumeVm(config) {
|
|
|
788
994
|
return {
|
|
789
995
|
vm,
|
|
790
996
|
credentials: creds,
|
|
997
|
+
secretManager,
|
|
791
998
|
mountPath: config.mountPath,
|
|
792
999
|
guestWorkspace,
|
|
793
1000
|
agentDir
|
|
@@ -864,4 +1071,4 @@ function rewriteMoltnetJsonPaths(moltnetJson, vmAgentDir, vmSshDir, githubAppPem
|
|
|
864
1071
|
return JSON.stringify(config);
|
|
865
1072
|
}
|
|
866
1073
|
//#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 };
|
|
1074
|
+
export { AutoParentMemoryProvider, BrokeredHttpSecretBoundaryError, GONDOLIN_BASE_EXECUTABLES, 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.2.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",
|