@framers/agentos-ext-meeting-browser 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +171 -2
- package/dist/BrowserMeetingProvider.d.ts +74 -0
- package/dist/BrowserMeetingProvider.d.ts.map +1 -0
- package/dist/BrowserMeetingProvider.js +136 -0
- package/dist/BrowserMeetingProvider.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +294 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +81 -0
- package/dist/protocol.js.map +1 -0
- package/dist/scheduler/channel.d.ts +45 -0
- package/dist/scheduler/channel.d.ts.map +1 -0
- package/dist/scheduler/channel.js +122 -0
- package/dist/scheduler/channel.js.map +1 -0
- package/dist/scheduler/docker.d.ts +86 -0
- package/dist/scheduler/docker.d.ts.map +1 -0
- package/dist/scheduler/docker.js +35 -0
- package/dist/scheduler/docker.js.map +1 -0
- package/dist/scheduler/main.d.ts +3 -0
- package/dist/scheduler/main.d.ts.map +1 -0
- package/dist/scheduler/main.js +79 -0
- package/dist/scheduler/main.js.map +1 -0
- package/dist/scheduler/runs.d.ts +75 -0
- package/dist/scheduler/runs.d.ts.map +1 -0
- package/dist/scheduler/runs.js +123 -0
- package/dist/scheduler/runs.js.map +1 -0
- package/dist/scheduler/server.d.ts +177 -0
- package/dist/scheduler/server.d.ts.map +1 -0
- package/dist/scheduler/server.js +653 -0
- package/dist/scheduler/server.js.map +1 -0
- package/dist/scheduler/urls.d.ts +28 -0
- package/dist/scheduler/urls.d.ts.map +1 -0
- package/dist/scheduler/urls.js +49 -0
- package/dist/scheduler/urls.js.map +1 -0
- package/manifest.json +25 -0
- package/package.json +73 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The label that names a container's run. */
|
|
2
|
+
export const LABEL_RUN = 'dev.frame.meeting.run';
|
|
3
|
+
/** The label that names the worker a container belongs to. */
|
|
4
|
+
export const LABEL_WORKER = 'dev.frame.meeting.worker';
|
|
5
|
+
/** Where an identity's profile is mounted inside the container. */
|
|
6
|
+
export const PROFILE_MOUNT = '/home/bot/profile';
|
|
7
|
+
/**
|
|
8
|
+
* The container's create options: nothing customer-supplied, only the run id and the channel's address. Its stdin stays
|
|
9
|
+
* open for one attached writer, which is how the run's token reaches the bot without appearing in `docker inspect`.
|
|
10
|
+
*/
|
|
11
|
+
export function containerOptions(policy, runId, workerId, profileDir) {
|
|
12
|
+
return {
|
|
13
|
+
Image: policy.image,
|
|
14
|
+
name: `meeting-bot-${runId}`,
|
|
15
|
+
Labels: { [LABEL_RUN]: runId, [LABEL_WORKER]: workerId },
|
|
16
|
+
Env: [`MEETING_CHANNEL_URL=${policy.channelUrl}`, `MEETING_RUN_ID=${runId}`],
|
|
17
|
+
OpenStdin: true,
|
|
18
|
+
StdinOnce: true,
|
|
19
|
+
HostConfig: {
|
|
20
|
+
NetworkMode: policy.network,
|
|
21
|
+
ReadonlyRootfs: true,
|
|
22
|
+
CapDrop: ['ALL'],
|
|
23
|
+
SecurityOpt: ['no-new-privileges', ...(policy.seccompProfile ? [`seccomp=${policy.seccompProfile}`] : [])],
|
|
24
|
+
Memory: policy.memoryBytes,
|
|
25
|
+
NanoCpus: policy.nanoCpus,
|
|
26
|
+
PidsLimit: policy.pidsLimit,
|
|
27
|
+
ShmSize: policy.shmBytes,
|
|
28
|
+
Tmpfs: { '/tmp': 'rw,noexec,nosuid,size=512m', '/home/bot/.cache': 'rw,noexec,nosuid,size=256m', '/run/pulse': 'rw,size=16m' },
|
|
29
|
+
Binds: profileDir ? [`${profileDir}:${PROFILE_MOUNT}:rw`] : [],
|
|
30
|
+
AutoRemove: false,
|
|
31
|
+
RestartPolicy: { Name: 'no' },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../../src/scheduler/docker.ts"],"names":[],"mappings":"AAiDA,8CAA8C;AAC9C,MAAM,CAAC,MAAM,SAAS,GAAG,uBAAuB,CAAC;AACjD,8DAA8D;AAC9D,MAAM,CAAC,MAAM,YAAY,GAAG,0BAA0B,CAAC;AACvD,mEAAmE;AACnE,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAuB,EAAE,KAAa,EAAE,QAAgB,EAAE,UAA8B;IACvH,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,eAAe,KAAK,EAAE;QAC5B,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE;QACxD,GAAG,EAAE,CAAC,uBAAuB,MAAM,CAAC,UAAU,EAAE,EAAE,kBAAkB,KAAK,EAAE,CAAC;QAC5E,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,UAAU,EAAE;YACV,WAAW,EAAE,MAAM,CAAC,OAAO;YAC3B,cAAc,EAAE,IAAI;YACpB,OAAO,EAAE,CAAC,KAAK,CAAC;YAChB,WAAW,EAAE,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,WAAW,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC1G,MAAM,EAAE,MAAM,CAAC,WAAW;YAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,MAAM,CAAC,QAAQ;YACxB,KAAK,EAAE,EAAE,MAAM,EAAE,4BAA4B,EAAE,kBAAkB,EAAE,4BAA4B,EAAE,YAAY,EAAE,aAAa,EAAE;YAC9H,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE;YAC9D,UAAU,EAAE,KAAK;YACjB,aAAa,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;SAC9B;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/scheduler/main.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import Dockerode from 'dockerode';
|
|
4
|
+
import { createLogger } from '@framers/agentos';
|
|
5
|
+
import { parseListenAddresses, Scheduler } from './server.js';
|
|
6
|
+
const log = createLogger('meeting-scheduler');
|
|
7
|
+
/** An environment variable the scheduler cannot start without. */
|
|
8
|
+
function required(name) {
|
|
9
|
+
const value = process.env[name]?.trim();
|
|
10
|
+
if (!value)
|
|
11
|
+
throw new Error(`${name} is not set`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/** An environment variable that may be absent; an empty value counts as absent. */
|
|
15
|
+
function optional(name) {
|
|
16
|
+
const value = process.env[name]?.trim();
|
|
17
|
+
return value ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
function describe(err) {
|
|
20
|
+
return err instanceof Error ? `${err.name}: ${err.message}` : 'unknown error';
|
|
21
|
+
}
|
|
22
|
+
/** Ends the process with the code once the logger has had a moment to write its last line. */
|
|
23
|
+
function exit(code) {
|
|
24
|
+
process.exitCode = code;
|
|
25
|
+
setTimeout(() => process.exit(code), 200).unref();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The scheduler's entry point, `agentos-meeting-scheduler`: its configuration from the environment, a Docker client on
|
|
29
|
+
* the local socket, and a stop on `SIGTERM` or `SIGINT` that leaves the containers running for the next start to take
|
|
30
|
+
* back. The client credential is read from the file `MEETING_CLIENT_TOKEN_FILE` names, never from the environment itself.
|
|
31
|
+
*/
|
|
32
|
+
async function main() {
|
|
33
|
+
const [listen, ...alsoListen] = parseListenAddresses(required('MEETING_LISTEN'));
|
|
34
|
+
const workerId = required('MEETING_WORKER_ID');
|
|
35
|
+
const slots = Number(required('MEETING_SLOTS'));
|
|
36
|
+
const clientToken = readFileSync(required('MEETING_CLIENT_TOKEN_FILE'), 'utf8').trim();
|
|
37
|
+
const policy = {};
|
|
38
|
+
const profilesRoot = optional('MEETING_PROFILES_ROOT');
|
|
39
|
+
if (profilesRoot)
|
|
40
|
+
policy.profilesRoot = profilesRoot;
|
|
41
|
+
const seccompFile = optional('MEETING_SECCOMP_PROFILE');
|
|
42
|
+
// the Engine API takes the profile's JSON in the security option, as the docker command sends it, not the file's path
|
|
43
|
+
if (seccompFile)
|
|
44
|
+
policy.seccompProfile = JSON.stringify(JSON.parse(readFileSync(seccompFile, 'utf8')));
|
|
45
|
+
const scheduler = new Scheduler({
|
|
46
|
+
workerId,
|
|
47
|
+
slots,
|
|
48
|
+
clientToken,
|
|
49
|
+
image: required('MEETING_BOT_IMAGE'),
|
|
50
|
+
docker: new Dockerode(),
|
|
51
|
+
listen,
|
|
52
|
+
alsoListen,
|
|
53
|
+
channelUrl: optional('MEETING_CHANNEL_URL'),
|
|
54
|
+
stateRoot: optional('MEETING_STATE_ROOT'),
|
|
55
|
+
policy,
|
|
56
|
+
testHosts: (optional('MEETING_RESOLVE_ALLOW') ?? '').split(',').map((host) => host.trim()).filter(Boolean),
|
|
57
|
+
logger: log,
|
|
58
|
+
});
|
|
59
|
+
await scheduler.start();
|
|
60
|
+
log.info('the meeting scheduler is listening', { workerId, slots, addresses: alsoListen.length + 1 });
|
|
61
|
+
let stopping = false;
|
|
62
|
+
const stop = (signal) => {
|
|
63
|
+
if (stopping)
|
|
64
|
+
return;
|
|
65
|
+
stopping = true;
|
|
66
|
+
log.info('stopping the meeting scheduler; its containers keep running', { signal });
|
|
67
|
+
scheduler.stop().then(() => exit(0), (err) => {
|
|
68
|
+
log.error('the meeting scheduler did not stop cleanly', { error: describe(err) });
|
|
69
|
+
exit(1);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
73
|
+
process.on('SIGINT', () => stop('SIGINT'));
|
|
74
|
+
}
|
|
75
|
+
main().catch((err) => {
|
|
76
|
+
log.error('the meeting scheduler did not start', { error: describe(err) });
|
|
77
|
+
exit(1);
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/scheduler/main.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { JobMessage, RunStatus } from '../protocol.js';
|
|
2
|
+
/** One run the scheduler leased: its job, its token, where its reports go, and what the scheduler knows of its container. */
|
|
3
|
+
export interface LeasedRun {
|
|
4
|
+
/** The job the container receives as its channel's first message. */
|
|
5
|
+
job: JobMessage;
|
|
6
|
+
/** The run's bearer: the container presents it on the channel, and the scheduler forwards every report under it. */
|
|
7
|
+
workerToken: string;
|
|
8
|
+
/** Where the run's reports are posted. */
|
|
9
|
+
reportUrl: string;
|
|
10
|
+
/** Where the run is in the scheduler's eyes. */
|
|
11
|
+
state: RunStatus['state'];
|
|
12
|
+
/** When the scheduler leased the run (milliseconds since the epoch). */
|
|
13
|
+
startedAt: number;
|
|
14
|
+
/** When the scheduler saw the container exit. */
|
|
15
|
+
exitedAt?: number;
|
|
16
|
+
/** The container's id, once it is created. */
|
|
17
|
+
containerId?: string;
|
|
18
|
+
/** The identity whose profile a Meet run holds (a folder under the profiles root). */
|
|
19
|
+
identity?: string;
|
|
20
|
+
/** When the scheduler took a `leave` for the run: the kill timer counts from here. */
|
|
21
|
+
leaveCommandedAt?: number;
|
|
22
|
+
/** The reason of that `leave`, sent down a channel the bot opens after it. */
|
|
23
|
+
leaveReason?: string;
|
|
24
|
+
/** Whether the bot reported its own `done` or `crashed`, so the exit adds no second final report. */
|
|
25
|
+
finalReported?: boolean;
|
|
26
|
+
/** The highest report sequence number seen or sent for the run. */
|
|
27
|
+
reportSeq: number;
|
|
28
|
+
}
|
|
29
|
+
/** Whether two tokens are the same, compared in constant time for equal lengths. */
|
|
30
|
+
export declare function sameToken(a: string, b: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The scheduler's table of leased runs: a slot count, the runs by id, the token lookup for the channel's handshake.
|
|
33
|
+
* With a `stateRoot` (a folder on the worker's encrypted volume, mode 0700) every run's material is written to
|
|
34
|
+
* `<stateRoot>/<runId>.json` (mode 0600) as it changes and removed with the run, so a scheduler that restarts takes its
|
|
35
|
+
* runs back with their tokens, destinations, limits, start times and report sequences (the adoption of `server.ts`).
|
|
36
|
+
*/
|
|
37
|
+
export declare class RunTable {
|
|
38
|
+
readonly slots: number;
|
|
39
|
+
private readonly stateRoot?;
|
|
40
|
+
private readonly now;
|
|
41
|
+
private readonly runs;
|
|
42
|
+
/**
|
|
43
|
+
* @param slots - How many runs may be live at once.
|
|
44
|
+
* @param stateRoot - The folder the runs' state files live in; absent, nothing is written.
|
|
45
|
+
* @param now - The clock the listed seconds are counted on.
|
|
46
|
+
*/
|
|
47
|
+
constructor(slots: number, stateRoot?: string | undefined, now?: () => number);
|
|
48
|
+
/** How many runs hold a slot: every run that has not exited. */
|
|
49
|
+
get busy(): number;
|
|
50
|
+
/** Takes a slot for a new run and writes its state file; false when the slots are full or the run is already held. */
|
|
51
|
+
lease(run: LeasedRun): boolean;
|
|
52
|
+
/** Takes a run back from its state file after a restart, slots or no slots: it is already running. */
|
|
53
|
+
adopt(run: LeasedRun): void;
|
|
54
|
+
/** The run with this id, if the table holds it. */
|
|
55
|
+
get(runId: string): LeasedRun | undefined;
|
|
56
|
+
/** The live run whose token this is, compared in constant time; an exited run's token opens nothing. */
|
|
57
|
+
byToken(token: string): LeasedRun | undefined;
|
|
58
|
+
/** A run as `GET /runs` lists it; an exited run's seconds stop at its exit. */
|
|
59
|
+
status(run: LeasedRun): RunStatus;
|
|
60
|
+
/** Every run as `GET /runs` lists it. */
|
|
61
|
+
list(): RunStatus[];
|
|
62
|
+
/**
|
|
63
|
+
* Called after any change to a run's fields that a restart must see (state, container id, report sequence). Written
|
|
64
|
+
* to a temporary file and renamed, so a crash mid-write leaves the previous state, never half a file. A run the table
|
|
65
|
+
* no longer holds is never written again.
|
|
66
|
+
*/
|
|
67
|
+
persist(run: LeasedRun): void;
|
|
68
|
+
/** Drops the run from the table and deletes its state file. */
|
|
69
|
+
remove(runId: string): void;
|
|
70
|
+
/** Every run the table holds. */
|
|
71
|
+
values(): LeasedRun[];
|
|
72
|
+
/** The runs a previous scheduler wrote: what `adopt()` in the server matches against the containers it finds. */
|
|
73
|
+
static readStateFiles(stateRoot: string): LeasedRun[];
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=runs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/scheduler/runs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEvD,6HAA6H;AAC7H,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,GAAG,EAAE,UAAU,CAAC;IAChB,oHAAoH;IACpH,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC1B,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CACnB;AASD,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAIvD;AAED;;;;;GAKG;AACH,qBAAa,QAAQ;IAQP,QAAQ,CAAC,KAAK,EAAE,MAAM;IAAE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAAU,OAAO,CAAC,QAAQ,CAAC,GAAG;IAP7F,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IAErD;;;;OAIG;gBACkB,KAAK,EAAE,MAAM,EAAmB,SAAS,CAAC,EAAE,MAAM,YAAA,EAAmB,GAAG,GAAE,MAAM,MAAiB;IAItH,gEAAgE;IAChE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sHAAsH;IACtH,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO;IAQ9B,sGAAsG;IACtG,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI;IAI3B,mDAAmD;IACnD,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAIzC,wGAAwG;IACxG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAO7C,+EAA+E;IAC/E,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS;IAIjC,yCAAyC;IACzC,IAAI,IAAI,SAAS,EAAE;IAInB;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI;IAO7B,+DAA+D;IAC/D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK3B,iCAAiC;IACjC,MAAM,IAAI,SAAS,EAAE;IAIrB,iHAAiH;IACjH,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;CAiBtD"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { JobMessage, RunStatus } from '../protocol.js';
|
|
5
|
+
/** Whether a parsed state file holds a whole run: a job the channel can send, its token, its destination and its counters. */
|
|
6
|
+
function isLeasedRun(value) {
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return false;
|
|
9
|
+
const run = value;
|
|
10
|
+
return JobMessage.safeParse(run.job).success && typeof run.workerToken === 'string' && typeof run.reportUrl === 'string' && RunStatus.shape.state.safeParse(run.state).success && typeof run.startedAt === 'number' && typeof run.reportSeq === 'number';
|
|
11
|
+
}
|
|
12
|
+
/** Whether two tokens are the same, compared in constant time for equal lengths. */
|
|
13
|
+
export function sameToken(a, b) {
|
|
14
|
+
const left = Buffer.from(a);
|
|
15
|
+
const right = Buffer.from(b);
|
|
16
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The scheduler's table of leased runs: a slot count, the runs by id, the token lookup for the channel's handshake.
|
|
20
|
+
* With a `stateRoot` (a folder on the worker's encrypted volume, mode 0700) every run's material is written to
|
|
21
|
+
* `<stateRoot>/<runId>.json` (mode 0600) as it changes and removed with the run, so a scheduler that restarts takes its
|
|
22
|
+
* runs back with their tokens, destinations, limits, start times and report sequences (the adoption of `server.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export class RunTable {
|
|
25
|
+
slots;
|
|
26
|
+
stateRoot;
|
|
27
|
+
now;
|
|
28
|
+
runs = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* @param slots - How many runs may be live at once.
|
|
31
|
+
* @param stateRoot - The folder the runs' state files live in; absent, nothing is written.
|
|
32
|
+
* @param now - The clock the listed seconds are counted on.
|
|
33
|
+
*/
|
|
34
|
+
constructor(slots, stateRoot, now = Date.now) {
|
|
35
|
+
this.slots = slots;
|
|
36
|
+
this.stateRoot = stateRoot;
|
|
37
|
+
this.now = now;
|
|
38
|
+
if (stateRoot)
|
|
39
|
+
mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
/** How many runs hold a slot: every run that has not exited. */
|
|
42
|
+
get busy() {
|
|
43
|
+
return [...this.runs.values()].filter((r) => r.state !== 'exited').length;
|
|
44
|
+
}
|
|
45
|
+
/** Takes a slot for a new run and writes its state file; false when the slots are full or the run is already held. */
|
|
46
|
+
lease(run) {
|
|
47
|
+
if (this.busy >= this.slots)
|
|
48
|
+
return false;
|
|
49
|
+
if (this.runs.has(run.job.runId))
|
|
50
|
+
return false;
|
|
51
|
+
this.runs.set(run.job.runId, run);
|
|
52
|
+
this.persist(run);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** Takes a run back from its state file after a restart, slots or no slots: it is already running. */
|
|
56
|
+
adopt(run) {
|
|
57
|
+
this.runs.set(run.job.runId, run);
|
|
58
|
+
}
|
|
59
|
+
/** The run with this id, if the table holds it. */
|
|
60
|
+
get(runId) {
|
|
61
|
+
return this.runs.get(runId);
|
|
62
|
+
}
|
|
63
|
+
/** The live run whose token this is, compared in constant time; an exited run's token opens nothing. */
|
|
64
|
+
byToken(token) {
|
|
65
|
+
for (const r of this.runs.values()) {
|
|
66
|
+
if (r.state !== 'exited' && sameToken(r.workerToken, token))
|
|
67
|
+
return r;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** A run as `GET /runs` lists it; an exited run's seconds stop at its exit. */
|
|
72
|
+
status(run) {
|
|
73
|
+
return { runId: run.job.runId, state: run.state, startedAt: run.startedAt, seconds: Math.round(((run.exitedAt ?? this.now()) - run.startedAt) / 1000), containerId: run.containerId };
|
|
74
|
+
}
|
|
75
|
+
/** Every run as `GET /runs` lists it. */
|
|
76
|
+
list() {
|
|
77
|
+
return [...this.runs.values()].map((r) => this.status(r));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Called after any change to a run's fields that a restart must see (state, container id, report sequence). Written
|
|
81
|
+
* to a temporary file and renamed, so a crash mid-write leaves the previous state, never half a file. A run the table
|
|
82
|
+
* no longer holds is never written again.
|
|
83
|
+
*/
|
|
84
|
+
persist(run) {
|
|
85
|
+
if (!this.stateRoot || this.runs.get(run.job.runId) !== run)
|
|
86
|
+
return;
|
|
87
|
+
const file = join(this.stateRoot, `${run.job.runId}.json`);
|
|
88
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(run), { mode: 0o600 });
|
|
89
|
+
renameSync(`${file}.tmp`, file);
|
|
90
|
+
}
|
|
91
|
+
/** Drops the run from the table and deletes its state file. */
|
|
92
|
+
remove(runId) {
|
|
93
|
+
this.runs.delete(runId);
|
|
94
|
+
if (this.stateRoot)
|
|
95
|
+
rmSync(join(this.stateRoot, `${runId}.json`), { force: true });
|
|
96
|
+
}
|
|
97
|
+
/** Every run the table holds. */
|
|
98
|
+
values() {
|
|
99
|
+
return [...this.runs.values()];
|
|
100
|
+
}
|
|
101
|
+
/** The runs a previous scheduler wrote: what `adopt()` in the server matches against the containers it finds. */
|
|
102
|
+
static readStateFiles(stateRoot) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const name of readdirSync(stateRoot)) {
|
|
105
|
+
if (name.endsWith('.json.tmp')) {
|
|
106
|
+
rmSync(join(stateRoot, name), { force: true }); // a write cut off before its rename: the previous state file stands
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!name.endsWith('.json'))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
const run = JSON.parse(readFileSync(join(stateRoot, name), 'utf8'));
|
|
113
|
+
if (isLeasedRun(run))
|
|
114
|
+
out.push(run);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// a file that does not parse names no run: the container it named is stopped by adopt(), since no run claims it
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=runs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../../src/scheduler/runs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { ILogger } from '@framers/agentos';
|
|
2
|
+
import { type ContainerPolicy, type DockerLike } from './docker.js';
|
|
3
|
+
import { type Resolver } from './urls.js';
|
|
4
|
+
/** The file an identity's provisioning writes in its folder once its profile is signed in; a folder without it is not in the pool. */
|
|
5
|
+
export declare const PROVISIONED_MARKER = "provisioned";
|
|
6
|
+
/** The file that takes an identity out of the pool until it is signed in again; the bot's `needs_reauth` error report writes it. */
|
|
7
|
+
export declare const NEEDS_REAUTH_MARKER = "needs_reauth";
|
|
8
|
+
/** An address the scheduler listens on. */
|
|
9
|
+
export interface ListenAddress {
|
|
10
|
+
/** The interface's address (`127.0.0.1`, the bots' bridge gateway, `::1`). */
|
|
11
|
+
host: string;
|
|
12
|
+
/** The port; `0` takes a free one. */
|
|
13
|
+
port: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads a list of listen addresses: `host:port` entries separated by commas (`127.0.0.1:8788,10.90.0.1:8788`), an IPv6
|
|
17
|
+
* host in brackets (`[::1]:8788`). Throws on an entry that is not one, and on an empty list.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseListenAddresses(text: string): ListenAddress[];
|
|
20
|
+
/** What a {@link Scheduler} runs with: the worker's id and slots, the client credential, the image and Docker, its addresses. */
|
|
21
|
+
export interface SchedulerOptions {
|
|
22
|
+
/** The worker's id: on every forwarded report, in `GET /capacity`, and on its containers' labels. */
|
|
23
|
+
workerId: string;
|
|
24
|
+
/** How many runs the worker holds at once. */
|
|
25
|
+
slots: number;
|
|
26
|
+
/** The product's client credential, at least 16 characters: every route but `GET /health` takes it as a bearer token. */
|
|
27
|
+
clientToken: string;
|
|
28
|
+
/** The bot image, by tag or digest. */
|
|
29
|
+
image: string;
|
|
30
|
+
/** The Docker client. */
|
|
31
|
+
docker: DockerLike;
|
|
32
|
+
/** The first address the scheduler listens on, which {@link Scheduler.url} and {@link Scheduler.wsUrl} name. */
|
|
33
|
+
listen: ListenAddress;
|
|
34
|
+
/** More addresses for the same routes (the bots' bridge gateway, beside the reverse proxy's loopback address). */
|
|
35
|
+
alsoListen?: readonly ListenAddress[];
|
|
36
|
+
/** The channel url the containers reach (the scheduler on the bots' network). Default `ws://<host>:<port>/channel`. */
|
|
37
|
+
channelUrl?: string;
|
|
38
|
+
/** The folder the runs' state files live in (the encrypted volume); absent, a restart adopts nothing and stops every container of ours. */
|
|
39
|
+
stateRoot?: string;
|
|
40
|
+
/** The container policy's limits, its seccomp profile and the identity pool's folder; the image and the channel url are the options above. */
|
|
41
|
+
policy?: Partial<Omit<ContainerPolicy, 'image' | 'channelUrl'>>;
|
|
42
|
+
/** Resolves a join host to its addresses. Default the system resolver. */
|
|
43
|
+
resolve?: Resolver;
|
|
44
|
+
/** Test hosts whose links are taken over http at any address and sent to the fixture adapter (`MEETING_RESOLVE_ALLOW`); empty in production. */
|
|
45
|
+
testHosts?: readonly string[];
|
|
46
|
+
/** How long a bot told to leave has to exit before it is killed. Default 30 seconds. */
|
|
47
|
+
killAfterMs?: number;
|
|
48
|
+
/** How long a started bot has to open its channel before it is killed. Default two minutes. */
|
|
49
|
+
connectWithinMs?: number;
|
|
50
|
+
/** How often the sweeper runs. Default five seconds. */
|
|
51
|
+
sweepEveryMs?: number;
|
|
52
|
+
/** The clock. Default `Date.now`. */
|
|
53
|
+
now?: () => number;
|
|
54
|
+
/** The `fetch` reports are posted with. Default the global `fetch`. */
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
/** The host's logger. Its lines carry run ids, states, reasons and counts, never a link, a name, a chat line or a token. */
|
|
57
|
+
logger?: ILogger;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The runner on a worker machine. It is not the source of truth (the meeting manager's job store is): it leases a slot
|
|
61
|
+
* per run, starts one container per run under a fixed policy, holds the container's command channel, relays the
|
|
62
|
+
* product's commands down it, forwards the container's reports to the product under the run's token, and answers
|
|
63
|
+
* `GET /runs` from the runs it holds, which after a restart are the ones its state files and Docker agree on.
|
|
64
|
+
*
|
|
65
|
+
* Its boundary: the product's requests carry the client credential and name a run this scheduler leased; nothing
|
|
66
|
+
* customer-supplied reaches Docker but the job, which travels over the channel, and the run's token, which travels on the
|
|
67
|
+
* container's stdin; the image and the limits are this configuration's; a join link is checked before a container
|
|
68
|
+
* starts (the platform's host, no address in a private range).
|
|
69
|
+
*/
|
|
70
|
+
export declare class Scheduler {
|
|
71
|
+
private readonly options;
|
|
72
|
+
private readonly servers;
|
|
73
|
+
private readonly wss;
|
|
74
|
+
private readonly runs;
|
|
75
|
+
private readonly peers;
|
|
76
|
+
private readonly forwarding;
|
|
77
|
+
private readonly policy;
|
|
78
|
+
private readonly testHosts;
|
|
79
|
+
private readonly fetch;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly log;
|
|
82
|
+
private resolver;
|
|
83
|
+
private paused;
|
|
84
|
+
private stopped;
|
|
85
|
+
private sweeping;
|
|
86
|
+
private port;
|
|
87
|
+
private sweeper;
|
|
88
|
+
/** @param options - The worker's id, its slots, the client credential, the image, the Docker client and where it listens. */
|
|
89
|
+
constructor(options: SchedulerOptions);
|
|
90
|
+
/** The HTTP base url on the first listen address, with the port it was given or the one it was assigned. */
|
|
91
|
+
get url(): string;
|
|
92
|
+
/** The same address for WebSockets: the command channel is `<wsUrl>/channel`. */
|
|
93
|
+
get wsUrl(): string;
|
|
94
|
+
/** Replaces the resolver the join-link check uses. */
|
|
95
|
+
setResolver(r: Resolver): void;
|
|
96
|
+
/** Listens on every address, takes back the runs a previous scheduler left, and starts the sweeper. */
|
|
97
|
+
start(): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Stops listening and closes every channel (`1001`). The containers keep running, and the next scheduler on the same
|
|
100
|
+
* state folder takes them back. A stopped scheduler acts on nothing more: no report, no state write, no kill.
|
|
101
|
+
*/
|
|
102
|
+
stop(): Promise<void>;
|
|
103
|
+
/** Takes no new run; `GET /capacity` says so. The runs it holds go on. */
|
|
104
|
+
pause(): void;
|
|
105
|
+
/** Takes new runs again. */
|
|
106
|
+
resume(): void;
|
|
107
|
+
private addresses;
|
|
108
|
+
/**
|
|
109
|
+
* After a restart: every run with a state file is taken back whole (its token, its destination, its limits, its start
|
|
110
|
+
* time, its report sequence), its container's exit is watched again, and its bot's channel reconnects with the token it
|
|
111
|
+
* holds; a container of ours with no state file is stopped and removed (nothing can command or report it); a state
|
|
112
|
+
* file whose container is gone is reported `crashed` to the product, unless it had already exited.
|
|
113
|
+
*/
|
|
114
|
+
private adopt;
|
|
115
|
+
private authorized;
|
|
116
|
+
/**
|
|
117
|
+
* The request's JSON body. One over the limit is read to its end and dropped, so the 413 reaches the client (leaving a
|
|
118
|
+
* stream's iterator early destroys the socket the answer would go out on).
|
|
119
|
+
*/
|
|
120
|
+
private body;
|
|
121
|
+
private json;
|
|
122
|
+
private route;
|
|
123
|
+
private refuse;
|
|
124
|
+
private startRun;
|
|
125
|
+
/**
|
|
126
|
+
* A later dispatch of a run this scheduler holds: the manager minted a new token for it, so the earlier container,
|
|
127
|
+
* whose token the product no longer accepts, is stopped, and the run starts again under the new token. Nothing is
|
|
128
|
+
* reported for the earlier container.
|
|
129
|
+
*/
|
|
130
|
+
private replace;
|
|
131
|
+
private commandRun;
|
|
132
|
+
private deleteRun;
|
|
133
|
+
private killContainer;
|
|
134
|
+
/** Watches a container's exit; a watch that fails (the Docker socket lost) stops the container, so no bot outlives its watch. */
|
|
135
|
+
private watch;
|
|
136
|
+
/**
|
|
137
|
+
* A container's exit: the run is marked exited, its channel closed, and `done` (exit 0) or `crashed` reported, unless
|
|
138
|
+
* the bot reported its own end first; the container is removed. The exit of a container whose run was deleted or
|
|
139
|
+
* dispatched again only removes it.
|
|
140
|
+
*/
|
|
141
|
+
private onExit;
|
|
142
|
+
private seconds;
|
|
143
|
+
/** A report the scheduler makes itself (an exit, a lost container): numbered after the bot's last, and queued behind it. */
|
|
144
|
+
private report;
|
|
145
|
+
/**
|
|
146
|
+
* A run told to leave that has not exited within `killAfterMs` is killed; a bot that has not opened its channel within
|
|
147
|
+
* `connectWithinMs` of its start is killed (it never had its job); a run alive a minute past its whole length is killed
|
|
148
|
+
* too (the limit plus sixty seconds, enforced here whatever the bot or the manager did); the exit path reports each as
|
|
149
|
+
* `crashed`. An exited run is forgotten an hour after its exit.
|
|
150
|
+
*/
|
|
151
|
+
private sweep;
|
|
152
|
+
private upgrade;
|
|
153
|
+
/**
|
|
154
|
+
* A bot's channel, after its token named a live run: it is the run's one channel (a second is closed with `4409`), it
|
|
155
|
+
* gets the job as its first message, its reports are forwarded, and a leave taken while it was away goes down it next.
|
|
156
|
+
*/
|
|
157
|
+
private connect;
|
|
158
|
+
private onReport;
|
|
159
|
+
/** The bot found its identity signed out or challenged: the identity's folder gets the marker, and the identity leaves the pool. */
|
|
160
|
+
private markNeedsReauth;
|
|
161
|
+
/** Reports of one run go to the product one at a time, in the order they came, so a `done` never overtakes the `left` before it. */
|
|
162
|
+
private enqueue;
|
|
163
|
+
/**
|
|
164
|
+
* Reports go to the product with the run's own token; a failed post is tried three times in all, then logged by run
|
|
165
|
+
* id, sequence and event (never content). A refusal (a 4xx but 429) is not tried again.
|
|
166
|
+
*/
|
|
167
|
+
private forward;
|
|
168
|
+
/**
|
|
169
|
+
* The identity for a new Meet run: the first folder under the profiles root (in name order, `notes2` before `notes10`)
|
|
170
|
+
* that its provisioning marked, that is not marked for signing in again, and that no live run holds: one run per
|
|
171
|
+
* identity.
|
|
172
|
+
*/
|
|
173
|
+
private pickIdentity;
|
|
174
|
+
/** The run's token as one line on the container's stdin, which is then closed: the bot reads it to the end before anything else. */
|
|
175
|
+
private writeStdin;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/scheduler/server.ts"],"names":[],"mappings":"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"}
|