@capsulos/sdk 0.2.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/dist/capsule.js +30 -14
- package/dist/capsule.js.map +1 -1
- package/dist/client.d.ts +7 -3
- package/dist/client.js +3 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +7 -0
- package/dist/network.js +37 -0
- package/dist/network.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/capsule.ts +30 -15
- package/src/client.ts +10 -3
- package/src/index.ts +1 -0
- package/src/network.test.ts +101 -0
- package/src/network.ts +34 -0
- package/src/types.ts +32 -0
package/dist/capsule.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The public face of a capsule. `Capsule.create()` asks the provider for a
|
|
2
2
|
// machine and waits until capsulod answers; everything after that goes
|
|
3
3
|
// straight to the daemon.
|
|
4
|
+
import { validateNetworkPolicy } from './network.js';
|
|
4
5
|
import { DaemonClient } from './client.js';
|
|
5
6
|
import { Ports } from './ports.js';
|
|
6
7
|
import { PtySession } from './pty.js';
|
|
@@ -27,23 +28,38 @@ export class Capsule {
|
|
|
27
28
|
}
|
|
28
29
|
/** Throw a capsule: create it with the provider and wait until it is ready. */
|
|
29
30
|
static async create(provider, spec, opts = {}) {
|
|
31
|
+
const network = validateNetworkPolicy(spec.network);
|
|
30
32
|
const handle = await provider.create(spec, { timeoutMs: opts.createTimeoutMs, signal: opts.signal });
|
|
31
33
|
const client = new DaemonClient(handle.connection, opts);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
try {
|
|
35
|
+
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000, opts.signal);
|
|
36
|
+
// Providers deliver mounts and network policy through their boot hook.
|
|
37
|
+
// When that did not happen (no hook, or the hook came without its
|
|
38
|
+
// payload, or the policy failed there), hand it over directly. Either
|
|
39
|
+
// way, a mount that failed or a policy that is not enforced is an error
|
|
40
|
+
// here, not a surprise later.
|
|
41
|
+
let results = health.mounts ?? [];
|
|
42
|
+
let status = health.network;
|
|
43
|
+
const wanted = Object.keys(spec.mounts ?? {});
|
|
44
|
+
const applied = new Set(results.map((r) => r.path));
|
|
45
|
+
const hookMissed = !health.configured || wanted.some((p) => !applied.has(p)) || (network && !status?.enforced);
|
|
46
|
+
if (hookMissed && (spec.mounts || spec.metadata || network)) {
|
|
47
|
+
({ mounts: results, network: status } = await client.configure({ mounts: spec.mounts, metadata: spec.metadata, network }));
|
|
48
|
+
}
|
|
49
|
+
const failed = results.filter((r) => !r.ok);
|
|
50
|
+
if (failed.length) {
|
|
51
|
+
throw new CapsuleError(`mounts failed: ${failed.map((r) => `${r.path} (${r.driver}): ${r.error}`).join('; ')}`);
|
|
52
|
+
}
|
|
53
|
+
if (network && !status?.enforced) {
|
|
54
|
+
throw new CapsuleError(`network policy not enforced: ${status?.error ?? 'the daemon reported no policy (is it 0.3.0 or newer?)'}`);
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
catch (err) {
|
|
58
|
+
// A capsule that did not get its spec is not handed out: it goes, and
|
|
59
|
+
// the error says which one it was.
|
|
60
|
+
await provider.terminate(handle.id).catch(() => { });
|
|
61
|
+
const e = err instanceof CapsuleError ? err : new CapsuleError(err instanceof Error ? err.message : String(err));
|
|
62
|
+
throw new CapsuleError(`${e.message} (capsule ${handle.id} terminated)`, e.status, { capsuleId: handle.id });
|
|
47
63
|
}
|
|
48
64
|
return new Capsule(handle.id, provider, client, opts.readyTimeoutMs);
|
|
49
65
|
}
|
package/dist/capsule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,0BAA0B;AAC1B,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EACL,YAAY,GASb,MAAM,YAAY,CAAC;AAWpB,MAAM,OAAO,OAAO;IAQP;IACQ;IACA;IACA;IAVV,QAAQ,CAAW;IACnB,KAAK,CAAQ;IACb,GAAG,CAAM;IAClB,yFAAyF;IAChF,KAAK,CAAQ;IAEtB,YACW,EAAU,EACF,QAAkB,EAClB,MAAoB,EACpB,iBAAiB,OAAO;QAHhC,OAAE,GAAF,EAAE,CAAQ;QACF,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAc;QACpB,mBAAc,GAAd,cAAc,CAAU;QAEzC,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5C,CAAC;IAED,+EAA+E;IAC/E,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAkB,EAAE,IAAiB,EAAE,OAAuB,EAAE;QAClF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACrG,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,IAAI,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,0BAA0B;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EACL,YAAY,GASb,MAAM,YAAY,CAAC;AAWpB,MAAM,OAAO,OAAO;IAQP;IACQ;IACA;IACA;IAVV,QAAQ,CAAW;IACnB,KAAK,CAAQ;IACb,GAAG,CAAM;IAClB,yFAAyF;IAChF,KAAK,CAAQ;IAEtB,YACW,EAAU,EACF,QAAkB,EAClB,MAAoB,EACpB,iBAAiB,OAAO;QAHhC,OAAE,GAAF,EAAE,CAAQ;QACF,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAc;QACpB,mBAAc,GAAd,cAAc,CAAU;QAEzC,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5C,CAAC;IAED,+EAA+E;IAC/E,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAkB,EAAE,IAAiB,EAAE,OAAuB,EAAE;QAClF,MAAM,OAAO,GAAG,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACrG,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACzD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,IAAI,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACpF,uEAAuE;YACvE,kEAAkE;YAClE,sEAAsE;YACtE,wEAAwE;YACxE,8BAA8B;YAC9B,IAAI,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;YAClC,IAAI,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;YAC9C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACpD,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAC/G,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,CAAC;gBAC5D,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;YAC7H,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClB,MAAM,IAAI,YAAY,CAAC,kBAAkB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClH,CAAC;YACD,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;gBACjC,MAAM,IAAI,YAAY,CAAC,gCAAgC,MAAM,EAAE,KAAK,IAAI,uDAAuD,EAAE,CAAC,CAAC;YACrI,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,sEAAsE;YACtE,mCAAmC;YACnC,MAAM,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACpD,MAAM,CAAC,GAAG,GAAG,YAAY,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACjH,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,OAAO,aAAa,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/G,CAAC;QACD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IACvE,CAAC;IAED,sGAAsG;IACtG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,QAAkB,EAAE,EAAU,EAAE,OAAuB,EAAE;QAC5E,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxC,OAAO,IAAI,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IACtF,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK;YAAE,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACpE,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACtE,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpC,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,2CAA2C;IAC3C,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC;CACF;AAED,MAAM,OAAO,QAAQ;IACU;IAA7B,YAA6B,MAAoB;QAApB,WAAM,GAAN,MAAM,CAAc;IAAG,CAAC;IACrD,sGAAsG;IACtG,GAAG,CAAC,GAAW,EAAE,IAAkB;QACjC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,GAAG;IACe;IAA7B,YAA6B,MAAoB;QAApB,WAAM,GAAN,MAAM,CAAc;IAAG,CAAC;IACrD,yDAAyD;IACzD,IAAI,CAAC,IAAiB;QACpB,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC;CACF;AAED,MAAM,OAAO,KAAK;IACa;IAA7B,YAA6B,MAAoB;QAApB,WAAM,GAAN,MAAM,CAAc;IAAG,CAAC;IACrD,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,SAAS,CAAC,IAAY;QACpB,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,IAAY,EAAE,IAAyB,EAAE,IAAwB;QACrE,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,KAAK,CAAC,IAAY;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IACD,MAAM,CAAC,IAAY;QACjB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;CACF;AAED,SAAS,WAAW,CAAC,QAAkB,EAAE,EAAU;IACjD,OAAO,IAAI,YAAY,CAAC,OAAO,QAAQ,CAAC,IAAI,8BAA8B,EAAE,IAAI,CAAC,CAAC;AACpF,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,MAAoB,EAAE,SAAiB,EAAE,MAAoB;IACpF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACtB,IAAI,OAAgB,CAAC;IACrB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,SAAS,EAAE,CAAC;QACnC,IAAI,MAAM,EAAE,OAAO;YAAE,MAAM,IAAI,YAAY,CAAC,qCAAqC,CAAC,CAAC;QACnF,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YAChC,IAAI,CAAC,CAAC,EAAE;gBAAE,OAAO,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,MAAM,IAAI,YAAY,CAAC,0CAA0C,SAAS,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACvG,CAAC"}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Connection, type ExecEvent, type ExecOptions, type ExecResult, type FileEntry, type Health, type MountResult, type MountSpec } from './types.js';
|
|
1
|
+
import { type Connection, type ExecEvent, type ExecOptions, type ExecResult, type FileEntry, type Health, type MountResult, type MountSpec, type NetworkPolicy, type NetworkStatus } from './types.js';
|
|
2
2
|
export interface ClientOptions {
|
|
3
3
|
fetch?: typeof fetch;
|
|
4
4
|
requestTimeoutMs?: number;
|
|
@@ -10,11 +10,15 @@ export declare class DaemonClient {
|
|
|
10
10
|
constructor(connection: Connection, opts?: ClientOptions);
|
|
11
11
|
private request;
|
|
12
12
|
health(): Promise<Health>;
|
|
13
|
-
/** Apply the mount table when the provider's boot hook did not deliver
|
|
13
|
+
/** Apply the mount table and network policy when the provider's boot hook did not deliver them. */
|
|
14
14
|
configure(body: {
|
|
15
15
|
mounts?: Record<string, MountSpec>;
|
|
16
16
|
metadata?: Record<string, string>;
|
|
17
|
-
|
|
17
|
+
network?: NetworkPolicy;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
mounts: MountResult[];
|
|
20
|
+
network?: NetworkStatus;
|
|
21
|
+
}>;
|
|
18
22
|
/** Buffered or streamed depending on whether callbacks are given. */
|
|
19
23
|
exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
20
24
|
readFile(path: string): Promise<Uint8Array>;
|
package/dist/client.js
CHANGED
|
@@ -37,7 +37,7 @@ export class DaemonClient {
|
|
|
37
37
|
const res = await this.request('/health', { timeoutMs: 10_000 });
|
|
38
38
|
return (await res.json());
|
|
39
39
|
}
|
|
40
|
-
/** Apply the mount table when the provider's boot hook did not deliver
|
|
40
|
+
/** Apply the mount table and network policy when the provider's boot hook did not deliver them. */
|
|
41
41
|
async configure(body) {
|
|
42
42
|
const res = await this.request('/configure', {
|
|
43
43
|
method: 'POST',
|
|
@@ -45,7 +45,8 @@ export class DaemonClient {
|
|
|
45
45
|
headers: { 'Content-Type': 'application/json' },
|
|
46
46
|
timeoutMs: 60_000,
|
|
47
47
|
});
|
|
48
|
-
|
|
48
|
+
const out = (await res.json());
|
|
49
|
+
return { mounts: out.mounts ?? [], network: out.network };
|
|
49
50
|
}
|
|
50
51
|
/** Buffered or streamed depending on whether callbacks are given. */
|
|
51
52
|
async exec(cmd, opts = {}) {
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,gFAAgF;AAChF,OAAO,EACL,YAAY,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,gFAAgF;AAChF,OAAO,EACL,YAAY,GAWb,MAAM,YAAY,CAAC;AAOpB,MAAM,OAAO,YAAY;IAKZ;IAJM,SAAS,CAAe;IACxB,gBAAgB,CAAS;IAE1C,YACW,UAAsB,EAC/B,OAAsB,EAAE;QADf,eAAU,GAAV,UAAU,CAAY;QAG/B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,MAAM,CAAC;IAC1D,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,OAA6C,EAAE,EAAE,KAAK,GAAG,KAAK;QAChG,MAAM,OAAO,GAAG,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,GAAI,IAAI,CAAC,OAA8C,EAAE,CAAC;QAC3H,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;YACpE,GAAG,IAAI;YACP,OAAO;YACP,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,gBAAgB,CAAC;SACrE,CAAC,CAAC;QACH,6FAA6F;QAC7F,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACzD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,GAAI,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAC,KAAK,IAAI,EAAE,CAAC;YAClE,CAAC;YAAC,MAAM,CAAC;gBACP,yBAAyB;YAC3B,CAAC;YACD,MAAM,IAAI,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,IAAI,WAAW,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3H,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAW,CAAC;IACtC,CAAC;IAED,mGAAmG;IACnG,KAAK,CAAC,SAAS,CAAC,IAIf;QACC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;YAC3C,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,SAAS,EAAE,MAAM;SAClB,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwD,CAAC;QACtF,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,OAAoB,EAAE;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,GAAG;YACH,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QACH,+EAA+E;QAC/E,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC;QACtD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE1D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC9H,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAe,CAAC;QAC1C,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,IAAI;YACJ,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,mBAAmB,EAAE;YAC5E,SAAS;SACV,CAAC,CAAC;QACH,MAAM,MAAM,GAAe,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;QACnG,IAAI,KAAK,EAAE,MAAM,EAAE,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACrC,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACzB,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,CAAC;gBACzB,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;iBAAM,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAChC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,CAAC;gBACzB,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC;gBACnC,MAAM,CAAC,OAAO,GAAG,EAAE,CAAC,OAAO,IAAI,KAAK,CAAC;gBACrC,IAAI,EAAE,CAAC,KAAK;oBAAE,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC;YACxC,CAAC;QACH,CAAC;QACD,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAC3C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,oBAAoB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/E,OAAO,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,IAAyB,EAAE,OAA0B,EAAE;QACnF,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACnF,MAAM,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,EAAE;YACtC,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,IAAiB;YAC1D,OAAO,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE;SACxD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,oBAAoB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/E,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAgB,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,oBAAoB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/E,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAc,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,IAAI,CAAC,OAAO,CAAC,qBAAqB,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1F,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtF,CAAC;CACF;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,QAAQ,CAAC,GAAa;IAC3C,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IACrC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,YAAY,CAAC,yBAAyB,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,SAAS,CAAC;QACR,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,IAAI,GAAW,CAAC;QAChB,OAAO,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACnC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;YACvE,IAAI,QAAQ;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAc,CAAC;QACjE,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAErD,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAErD,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type NetworkPolicy } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Check a network policy before a capsule is created with it (the daemon
|
|
4
|
+
* applies the same rules; failing here saves a launch). Returns the policy
|
|
5
|
+
* when it restricts something, undefined when it does not.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateNetworkPolicy(policy: NetworkPolicy | undefined): NetworkPolicy | undefined;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CapsuleError } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Check a network policy before a capsule is created with it (the daemon
|
|
4
|
+
* applies the same rules; failing here saves a launch). Returns the policy
|
|
5
|
+
* when it restricts something, undefined when it does not.
|
|
6
|
+
*/
|
|
7
|
+
export function validateNetworkPolicy(policy) {
|
|
8
|
+
if (!policy)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (policy.internet !== 'allow' && policy.internet !== 'deny') {
|
|
11
|
+
throw new CapsuleError(`network.internet must be 'allow' or 'deny', got ${JSON.stringify(policy.internet)}`);
|
|
12
|
+
}
|
|
13
|
+
if (policy.internet === 'allow') {
|
|
14
|
+
if (policy.allow?.length)
|
|
15
|
+
throw new CapsuleError("network.allow needs network.internet: 'deny'");
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of policy.allow ?? []) {
|
|
19
|
+
if (!isAllowEntry(entry)) {
|
|
20
|
+
throw new CapsuleError(`bad network.allow entry ${JSON.stringify(entry)} (want host, *.domain or host:port)`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return policy;
|
|
24
|
+
}
|
|
25
|
+
const HOST = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
26
|
+
function isAllowEntry(entry) {
|
|
27
|
+
if (typeof entry !== 'string')
|
|
28
|
+
return false;
|
|
29
|
+
const m = /^(.*?)(?::(\d{1,5}))?$/.exec(entry.trim());
|
|
30
|
+
if (!m)
|
|
31
|
+
return false;
|
|
32
|
+
const [, host, port] = m;
|
|
33
|
+
if (port !== undefined && (Number(port) < 1 || Number(port) > 65535))
|
|
34
|
+
return false;
|
|
35
|
+
return host !== undefined && HOST.test(host);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAiC;IACrE,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC9D,MAAM,IAAI,YAAY,CAAC,mDAAmD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC/G,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM;YAAE,MAAM,IAAI,YAAY,CAAC,8CAA8C,CAAC,CAAC;QACjG,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,YAAY,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAChH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,IAAI,GAAG,2EAA2E,CAAC;AAEzF,SAAS,YAAY,CAAC,KAAa;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnF,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -100,6 +100,7 @@ export interface Health {
|
|
|
100
100
|
/** True once the mount table was applied (by the boot hook or /configure). */
|
|
101
101
|
configured: boolean;
|
|
102
102
|
mounts?: MountResult[];
|
|
103
|
+
network?: NetworkStatus;
|
|
103
104
|
/** The template's long-running process (CAPSULOD_START), when the template declares one. */
|
|
104
105
|
service?: ServiceStatus;
|
|
105
106
|
}
|
|
@@ -111,6 +112,33 @@ export interface ServiceStatus {
|
|
|
111
112
|
lastExitCode?: number;
|
|
112
113
|
startedAt?: string;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* What the capsule may reach. With `internet: 'deny'` the daemon runs an
|
|
117
|
+
* egress proxy limited to `allow` (HTTP_PROXY/HTTPS_PROXY point at it for
|
|
118
|
+
* every process) and blackholes the default route, so a process that ignores
|
|
119
|
+
* the proxy gets "network unreachable" rather than the internet. Needs
|
|
120
|
+
* CAP_NET_ADMIN in the capsule: Lambda templates built with
|
|
121
|
+
* `--capabilities all`; the Docker provider adds it. Creation fails if the
|
|
122
|
+
* policy cannot be enforced.
|
|
123
|
+
*/
|
|
124
|
+
export interface NetworkPolicy {
|
|
125
|
+
/** 'allow' (default): unrestricted. 'deny': nothing leaves except `allow`. */
|
|
126
|
+
internet: 'allow' | 'deny';
|
|
127
|
+
/** Destinations still reachable with 'deny': 'host', '*.domain' (subdomains only) or 'host:port'. */
|
|
128
|
+
allow?: string[];
|
|
129
|
+
}
|
|
130
|
+
/** The daemon's view of the policy (`health().network`; absent without a policy). */
|
|
131
|
+
export interface NetworkStatus {
|
|
132
|
+
internet: 'allow' | 'deny';
|
|
133
|
+
allow?: string[];
|
|
134
|
+
/** True once the proxy runs and the routes are locked. */
|
|
135
|
+
enforced: boolean;
|
|
136
|
+
/** The egress proxy's address inside the capsule (127.0.0.1:3128). */
|
|
137
|
+
proxy?: string;
|
|
138
|
+
/** Connections refused so far. */
|
|
139
|
+
blocked: number;
|
|
140
|
+
error?: string;
|
|
141
|
+
}
|
|
114
142
|
export interface IdlePolicy {
|
|
115
143
|
/** Suspend after this long without traffic (seconds). */
|
|
116
144
|
suspendAfterSeconds: number;
|
|
@@ -126,6 +154,8 @@ export interface CapsuleSpec {
|
|
|
126
154
|
mounts?: Record<string, MountSpec>;
|
|
127
155
|
/** Opaque per-capsule configuration delivered to the daemon at boot (never secrets). */
|
|
128
156
|
metadata?: Record<string, string>;
|
|
157
|
+
/** Egress policy, enforced inside the capsule by the daemon (default: unrestricted). */
|
|
158
|
+
network?: NetworkPolicy;
|
|
129
159
|
/** Idempotency key: creating twice with the same key returns the same capsule. */
|
|
130
160
|
clientToken?: string;
|
|
131
161
|
}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;AA4NhF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAM1B;IALX,sGAAsG;IAC7F,SAAS,CAAU;IAE5B,YACE,OAAe,EACN,MAAe,EACxB,OAA+B,EAAE;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAS;QAIxB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACtD,CAAC;CACF"}
|
package/package.json
CHANGED
package/src/capsule.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The public face of a capsule. `Capsule.create()` asks the provider for a
|
|
2
2
|
// machine and waits until capsulod answers; everything after that goes
|
|
3
3
|
// straight to the daemon.
|
|
4
|
+
import { validateNetworkPolicy } from './network.js';
|
|
4
5
|
import { DaemonClient, type ClientOptions } from './client.js';
|
|
5
6
|
import { Ports } from './ports.js';
|
|
6
7
|
import { PtySession } from './pty.js';
|
|
@@ -46,23 +47,37 @@ export class Capsule {
|
|
|
46
47
|
|
|
47
48
|
/** Throw a capsule: create it with the provider and wait until it is ready. */
|
|
48
49
|
static async create(provider: Provider, spec: CapsuleSpec, opts: CapsuleOptions = {}): Promise<Capsule> {
|
|
50
|
+
const network = validateNetworkPolicy(spec.network);
|
|
49
51
|
const handle = await provider.create(spec, { timeoutMs: opts.createTimeoutMs, signal: opts.signal });
|
|
50
52
|
const client = new DaemonClient(handle.connection, opts);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
try {
|
|
54
|
+
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000, opts.signal);
|
|
55
|
+
// Providers deliver mounts and network policy through their boot hook.
|
|
56
|
+
// When that did not happen (no hook, or the hook came without its
|
|
57
|
+
// payload, or the policy failed there), hand it over directly. Either
|
|
58
|
+
// way, a mount that failed or a policy that is not enforced is an error
|
|
59
|
+
// here, not a surprise later.
|
|
60
|
+
let results = health.mounts ?? [];
|
|
61
|
+
let status = health.network;
|
|
62
|
+
const wanted = Object.keys(spec.mounts ?? {});
|
|
63
|
+
const applied = new Set(results.map((r) => r.path));
|
|
64
|
+
const hookMissed = !health.configured || wanted.some((p) => !applied.has(p)) || (network && !status?.enforced);
|
|
65
|
+
if (hookMissed && (spec.mounts || spec.metadata || network)) {
|
|
66
|
+
({ mounts: results, network: status } = await client.configure({ mounts: spec.mounts, metadata: spec.metadata, network }));
|
|
67
|
+
}
|
|
68
|
+
const failed = results.filter((r) => !r.ok);
|
|
69
|
+
if (failed.length) {
|
|
70
|
+
throw new CapsuleError(`mounts failed: ${failed.map((r) => `${r.path} (${r.driver}): ${r.error}`).join('; ')}`);
|
|
71
|
+
}
|
|
72
|
+
if (network && !status?.enforced) {
|
|
73
|
+
throw new CapsuleError(`network policy not enforced: ${status?.error ?? 'the daemon reported no policy (is it 0.3.0 or newer?)'}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// A capsule that did not get its spec is not handed out: it goes, and
|
|
77
|
+
// the error says which one it was.
|
|
78
|
+
await provider.terminate(handle.id).catch(() => {});
|
|
79
|
+
const e = err instanceof CapsuleError ? err : new CapsuleError(err instanceof Error ? err.message : String(err));
|
|
80
|
+
throw new CapsuleError(`${e.message} (capsule ${handle.id} terminated)`, e.status, { capsuleId: handle.id });
|
|
66
81
|
}
|
|
67
82
|
return new Capsule(handle.id, provider, client, opts.readyTimeoutMs);
|
|
68
83
|
}
|
package/src/client.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type Health,
|
|
11
11
|
type MountResult,
|
|
12
12
|
type MountSpec,
|
|
13
|
+
type NetworkPolicy,
|
|
14
|
+
type NetworkStatus,
|
|
13
15
|
} from './types.js';
|
|
14
16
|
|
|
15
17
|
export interface ClientOptions {
|
|
@@ -57,15 +59,20 @@ export class DaemonClient {
|
|
|
57
59
|
return (await res.json()) as Health;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
/** Apply the mount table when the provider's boot hook did not deliver
|
|
61
|
-
async configure(body: {
|
|
62
|
+
/** Apply the mount table and network policy when the provider's boot hook did not deliver them. */
|
|
63
|
+
async configure(body: {
|
|
64
|
+
mounts?: Record<string, MountSpec>;
|
|
65
|
+
metadata?: Record<string, string>;
|
|
66
|
+
network?: NetworkPolicy;
|
|
67
|
+
}): Promise<{ mounts: MountResult[]; network?: NetworkStatus }> {
|
|
62
68
|
const res = await this.request('/configure', {
|
|
63
69
|
method: 'POST',
|
|
64
70
|
body: JSON.stringify(body),
|
|
65
71
|
headers: { 'Content-Type': 'application/json' },
|
|
66
72
|
timeoutMs: 60_000,
|
|
67
73
|
});
|
|
68
|
-
|
|
74
|
+
const out = (await res.json()) as { mounts?: MountResult[]; network?: NetworkStatus };
|
|
75
|
+
return { mounts: out.mounts ?? [], network: out.network };
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/** Buffered or streamed depending on whether callbacks are given. */
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Capsule } from './capsule.js';
|
|
3
|
+
import { validateNetworkPolicy } from './network.js';
|
|
4
|
+
import type { CapsuleHandle, Provider } from './types.js';
|
|
5
|
+
|
|
6
|
+
describe('validateNetworkPolicy', () => {
|
|
7
|
+
it('accepts hosts, wildcards and host:port, and treats allow-all as no policy', () => {
|
|
8
|
+
expect(validateNetworkPolicy(undefined)).toBeUndefined();
|
|
9
|
+
expect(validateNetworkPolicy({ internet: 'allow' })).toBeUndefined();
|
|
10
|
+
const p = { internet: 'deny' as const, allow: ['api.anthropic.com', '*.npmjs.org', 'db.internal:5432', 'Host.IO'] };
|
|
11
|
+
expect(validateNetworkPolicy(p)).toBe(p);
|
|
12
|
+
expect(validateNetworkPolicy({ internet: 'deny' })).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects what the daemon would reject, before launching anything', () => {
|
|
16
|
+
expect(() => validateNetworkPolicy({ internet: 'maybe' as 'allow' })).toThrow(/must be 'allow' or 'deny'/);
|
|
17
|
+
expect(() => validateNetworkPolicy({ internet: 'allow', allow: ['x.com'] })).toThrow(/needs network.internet: 'deny'/);
|
|
18
|
+
for (const bad of ['https://x.com', '*', 'a.*.com', 'x.com:99999', 'x.com/path', '', 'x com']) {
|
|
19
|
+
expect(() => validateNetworkPolicy({ internet: 'deny', allow: [bad] }), bad).toThrow(/bad network.allow entry/);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function fakeProvider(health: Record<string, unknown>, configure: Record<string, unknown> | Error) {
|
|
25
|
+
const calls: string[] = [];
|
|
26
|
+
const fetchStub: typeof fetch = async (input, init) => {
|
|
27
|
+
const path = new URL(String(input)).pathname;
|
|
28
|
+
calls.push(`${init?.method ?? 'GET'} ${path}`);
|
|
29
|
+
if (path === '/health') return new Response(JSON.stringify({ ok: true, ...health }), { status: 200 });
|
|
30
|
+
if (configure instanceof Error) return new Response(JSON.stringify({ error: configure.message }), { status: 400 });
|
|
31
|
+
return new Response(JSON.stringify(configure), { status: 200 });
|
|
32
|
+
};
|
|
33
|
+
const handle: CapsuleHandle = {
|
|
34
|
+
id: 'c1',
|
|
35
|
+
provider: 'fake',
|
|
36
|
+
state: 'running',
|
|
37
|
+
template: 't',
|
|
38
|
+
createdAt: new Date(),
|
|
39
|
+
connection: { baseUrl: 'http://capsule', headers: async () => ({}) },
|
|
40
|
+
};
|
|
41
|
+
const provider: Provider = {
|
|
42
|
+
name: 'fake',
|
|
43
|
+
async create() {
|
|
44
|
+
calls.push('create');
|
|
45
|
+
return handle;
|
|
46
|
+
},
|
|
47
|
+
async get() {
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
async list() {
|
|
51
|
+
return [];
|
|
52
|
+
},
|
|
53
|
+
async terminate(id) {
|
|
54
|
+
calls.push(`terminate ${id}`);
|
|
55
|
+
},
|
|
56
|
+
async connect() {
|
|
57
|
+
return handle.connection;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return { provider, calls, fetch: fetchStub };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('Capsule.create with a network policy', () => {
|
|
64
|
+
const spec = { template: 't', network: { internet: 'deny' as const, allow: ['api.anthropic.com'] } };
|
|
65
|
+
|
|
66
|
+
it('is satisfied when the boot hook already enforced it', async () => {
|
|
67
|
+
const { provider, calls, fetch } = fakeProvider({ configured: true, network: { internet: 'deny', enforced: true, blocked: 0 } }, {});
|
|
68
|
+
const capsule = await Capsule.create(provider, spec, { fetch });
|
|
69
|
+
expect(capsule.id).toBe('c1');
|
|
70
|
+
expect(calls).toEqual(['create', 'GET /health']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('hands the policy over through /configure when the hook did not apply it', async () => {
|
|
74
|
+
const { provider, calls, fetch } = fakeProvider(
|
|
75
|
+
{ configured: false },
|
|
76
|
+
{ ok: true, mounts: [], network: { internet: 'deny', enforced: true, blocked: 0 } },
|
|
77
|
+
);
|
|
78
|
+
await Capsule.create(provider, spec, { fetch });
|
|
79
|
+
expect(calls).toEqual(['create', 'GET /health', 'POST /configure']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('terminates the capsule and says why when the policy cannot be enforced', async () => {
|
|
83
|
+
const { provider, calls, fetch } = fakeProvider(
|
|
84
|
+
{ configured: true, network: { internet: 'deny', enforced: false, blocked: 0, error: 'network policy needs CAP_NET_ADMIN' } },
|
|
85
|
+
new Error('network policy needs CAP_NET_ADMIN: build the template with --capabilities all'),
|
|
86
|
+
);
|
|
87
|
+
const err = (await Capsule.create(provider, spec, { fetch }).catch((e: unknown) => e)) as Error & { capsuleId?: string };
|
|
88
|
+
expect(err.message).toMatch(/CAP_NET_ADMIN/);
|
|
89
|
+
expect(err.message).toMatch(/capsule c1 terminated/);
|
|
90
|
+
expect(err.capsuleId).toBe('c1');
|
|
91
|
+
expect(calls.at(-1)).toBe('terminate c1');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects a bad policy before creating anything', async () => {
|
|
95
|
+
const { provider, calls, fetch } = fakeProvider({}, {});
|
|
96
|
+
await expect(Capsule.create(provider, { template: 't', network: { internet: 'deny', allow: ['*'] } }, { fetch })).rejects.toThrow(
|
|
97
|
+
/bad network.allow entry/,
|
|
98
|
+
);
|
|
99
|
+
expect(calls).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/network.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CapsuleError, type NetworkPolicy } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check a network policy before a capsule is created with it (the daemon
|
|
5
|
+
* applies the same rules; failing here saves a launch). Returns the policy
|
|
6
|
+
* when it restricts something, undefined when it does not.
|
|
7
|
+
*/
|
|
8
|
+
export function validateNetworkPolicy(policy: NetworkPolicy | undefined): NetworkPolicy | undefined {
|
|
9
|
+
if (!policy) return undefined;
|
|
10
|
+
if (policy.internet !== 'allow' && policy.internet !== 'deny') {
|
|
11
|
+
throw new CapsuleError(`network.internet must be 'allow' or 'deny', got ${JSON.stringify(policy.internet)}`);
|
|
12
|
+
}
|
|
13
|
+
if (policy.internet === 'allow') {
|
|
14
|
+
if (policy.allow?.length) throw new CapsuleError("network.allow needs network.internet: 'deny'");
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of policy.allow ?? []) {
|
|
18
|
+
if (!isAllowEntry(entry)) {
|
|
19
|
+
throw new CapsuleError(`bad network.allow entry ${JSON.stringify(entry)} (want host, *.domain or host:port)`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return policy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HOST = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
26
|
+
|
|
27
|
+
function isAllowEntry(entry: string): boolean {
|
|
28
|
+
if (typeof entry !== 'string') return false;
|
|
29
|
+
const m = /^(.*?)(?::(\d{1,5}))?$/.exec(entry.trim());
|
|
30
|
+
if (!m) return false;
|
|
31
|
+
const [, host, port] = m;
|
|
32
|
+
if (port !== undefined && (Number(port) < 1 || Number(port) > 65535)) return false;
|
|
33
|
+
return host !== undefined && HOST.test(host);
|
|
34
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,7 @@ export interface Health {
|
|
|
105
105
|
/** True once the mount table was applied (by the boot hook or /configure). */
|
|
106
106
|
configured: boolean;
|
|
107
107
|
mounts?: MountResult[];
|
|
108
|
+
network?: NetworkStatus;
|
|
108
109
|
/** The template's long-running process (CAPSULOD_START), when the template declares one. */
|
|
109
110
|
service?: ServiceStatus;
|
|
110
111
|
}
|
|
@@ -118,6 +119,35 @@ export interface ServiceStatus {
|
|
|
118
119
|
startedAt?: string;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* What the capsule may reach. With `internet: 'deny'` the daemon runs an
|
|
124
|
+
* egress proxy limited to `allow` (HTTP_PROXY/HTTPS_PROXY point at it for
|
|
125
|
+
* every process) and blackholes the default route, so a process that ignores
|
|
126
|
+
* the proxy gets "network unreachable" rather than the internet. Needs
|
|
127
|
+
* CAP_NET_ADMIN in the capsule: Lambda templates built with
|
|
128
|
+
* `--capabilities all`; the Docker provider adds it. Creation fails if the
|
|
129
|
+
* policy cannot be enforced.
|
|
130
|
+
*/
|
|
131
|
+
export interface NetworkPolicy {
|
|
132
|
+
/** 'allow' (default): unrestricted. 'deny': nothing leaves except `allow`. */
|
|
133
|
+
internet: 'allow' | 'deny';
|
|
134
|
+
/** Destinations still reachable with 'deny': 'host', '*.domain' (subdomains only) or 'host:port'. */
|
|
135
|
+
allow?: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The daemon's view of the policy (`health().network`; absent without a policy). */
|
|
139
|
+
export interface NetworkStatus {
|
|
140
|
+
internet: 'allow' | 'deny';
|
|
141
|
+
allow?: string[];
|
|
142
|
+
/** True once the proxy runs and the routes are locked. */
|
|
143
|
+
enforced: boolean;
|
|
144
|
+
/** The egress proxy's address inside the capsule (127.0.0.1:3128). */
|
|
145
|
+
proxy?: string;
|
|
146
|
+
/** Connections refused so far. */
|
|
147
|
+
blocked: number;
|
|
148
|
+
error?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
export interface IdlePolicy {
|
|
122
152
|
/** Suspend after this long without traffic (seconds). */
|
|
123
153
|
suspendAfterSeconds: number;
|
|
@@ -134,6 +164,8 @@ export interface CapsuleSpec {
|
|
|
134
164
|
mounts?: Record<string, MountSpec>;
|
|
135
165
|
/** Opaque per-capsule configuration delivered to the daemon at boot (never secrets). */
|
|
136
166
|
metadata?: Record<string, string>;
|
|
167
|
+
/** Egress policy, enforced inside the capsule by the daemon (default: unrestricted). */
|
|
168
|
+
network?: NetworkPolicy;
|
|
137
169
|
/** Idempotency key: creating twice with the same key returns the same capsule. */
|
|
138
170
|
clientToken?: string;
|
|
139
171
|
}
|