@volter/supercode-teams 0.2.20
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 +119 -0
- package/attention.mjs +191 -0
- package/bin/fleet.mjs +151 -0
- package/bin/teams.mjs +291 -0
- package/bin/workspace/args.mjs +66 -0
- package/bin/workspace/files.mjs +110 -0
- package/bin/workspace/lifecycle.mjs +113 -0
- package/bin/workspace/volter-device.mjs +42 -0
- package/bin/workspace/volter-login.mjs +185 -0
- package/bin/workspace.mjs +726 -0
- package/browser-attach.mjs +90 -0
- package/client.mjs +212 -0
- package/connector/effects.mjs +95 -0
- package/connector/index.d.ts +69 -0
- package/connector/index.mjs +174 -0
- package/connector/native-continuation.d.ts +8 -0
- package/connector/native-continuation.mjs +50 -0
- package/connector/native-effects.mjs +164 -0
- package/context.mjs +34 -0
- package/contracts/adapter-map.json +16 -0
- package/contracts/index.d.ts +299 -0
- package/contracts/index.mjs +37 -0
- package/execution.mjs +206 -0
- package/fleet.mjs +78 -0
- package/index.mjs +12 -0
- package/machine-door.mjs +36 -0
- package/machine.mjs +1398 -0
- package/package.json +80 -0
- package/porthole-hook.cjs +137 -0
- package/remote/job-exec.py +146 -0
- package/remote/wire.py +23 -0
- package/server/arc2-transactions.mjs +125 -0
- package/server/attachment-relay.mjs +149 -0
- package/server/catalog.mjs +7 -0
- package/server/continuations.mjs +56 -0
- package/server/correlation.d.ts +4 -0
- package/server/correlation.mjs +17 -0
- package/server/effects-transactions.mjs +119 -0
- package/server/effects.mjs +97 -0
- package/server/event-routes.mjs +208 -0
- package/server/exports.mjs +46 -0
- package/server/harness-routes.mjs +248 -0
- package/server/index.d.ts +31 -0
- package/server/index.mjs +52 -0
- package/server/ingest.mjs +24 -0
- package/server/node-upgrade.mjs +39 -0
- package/server/otlp/collector-config.d.ts +2 -0
- package/server/otlp/collector-config.mjs +26 -0
- package/server/otlp/decoders/index.d.ts +63 -0
- package/server/otlp/decoders/index.mjs +311 -0
- package/server/otlp/index.d.ts +4 -0
- package/server/otlp/index.mjs +4 -0
- package/server/otlp/projection.d.ts +5 -0
- package/server/otlp/projection.mjs +17 -0
- package/server/otlp/receiver.d.ts +3 -0
- package/server/otlp/receiver.mjs +36 -0
- package/server/otlp/schema/google/protobuf/any.proto +162 -0
- package/server/otlp/schema/google/rpc/status.proto +48 -0
- package/server/otlp/schema/manifest.json +22 -0
- package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
- package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
- package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
- package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
- package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
- package/server/otlp/transport.d.ts +10 -0
- package/server/otlp/transport.mjs +424 -0
- package/server/otlp/wire-codec.mjs +153 -0
- package/server/store-core.mjs +139 -0
- package/server/store-worker.mjs +23 -0
- package/worker/fetch-adapter.mjs +49 -0
- package/worker/index.mjs +127 -0
- package/worker/websocket-duplex.mjs +29 -0
- package/worker/wrangler.toml +38 -0
- package/workspace-client.d.ts +75 -0
- package/workspace-client.mjs +171 -0
- package/workspace-move.mjs +38 -0
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/supercode-teams",
|
|
3
|
+
"version": "0.2.20",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.mjs",
|
|
9
|
+
"./client": "./client.mjs",
|
|
10
|
+
"./machine": "./machine.mjs",
|
|
11
|
+
"./browser-attach": "./browser-attach.mjs",
|
|
12
|
+
"./attention": "./attention.mjs",
|
|
13
|
+
"./bin": "./bin/teams.mjs",
|
|
14
|
+
"./fleet": "./fleet.mjs",
|
|
15
|
+
"./bin/fleet": "./bin/fleet.mjs",
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./server/index.d.ts",
|
|
18
|
+
"default": "./server/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./workspace-client": {
|
|
21
|
+
"types": "./workspace-client.d.ts",
|
|
22
|
+
"default": "./workspace-client.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./connector": {
|
|
25
|
+
"types": "./connector/index.d.ts",
|
|
26
|
+
"default": "./connector/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./contracts": {
|
|
29
|
+
"types": "./contracts/index.d.ts",
|
|
30
|
+
"default": "./contracts/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./connector/native-continuation": {
|
|
33
|
+
"types": "./connector/native-continuation.d.ts",
|
|
34
|
+
"default": "./connector/native-continuation.mjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"supercode-teams": "./bin/teams.mjs",
|
|
39
|
+
"supercode-teams-fleet": "./bin/fleet.mjs"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"*.mjs",
|
|
43
|
+
"workspace-client.d.ts",
|
|
44
|
+
"porthole-hook.cjs",
|
|
45
|
+
"bin",
|
|
46
|
+
"server",
|
|
47
|
+
"worker",
|
|
48
|
+
"connector",
|
|
49
|
+
"contracts",
|
|
50
|
+
"remote",
|
|
51
|
+
"README.md"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck:teams": "tsc --noEmit -p test/tsconfig.json"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
61
|
+
"@volter/supercode-frontend": "^0.2.2",
|
|
62
|
+
"@volter/supercode-harness-sdk": "^0.3.43",
|
|
63
|
+
"@volter/supercode-terminal": "^0.2.30",
|
|
64
|
+
"@volter/identity": "^0.4.0",
|
|
65
|
+
"@volter/teams": "^0.1.6",
|
|
66
|
+
"protobufjs": "8.8.0",
|
|
67
|
+
"ws": "^8.21.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@xterm/xterm": "^6.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@xterm/xterm": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@grpc/grpc-js": "1.14.5"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The porthole hook: the node exports this file into every pane's
|
|
3
|
+
// NODE_OPTIONS (--require), so every node process an agent runs in a launched pane
|
|
4
|
+
// loads it. It patches child_process.spawn; when a Chromium launch goes by it makes
|
|
5
|
+
// sure a TCP DevTools port exists — Playwright's default pipe-only mode uses private
|
|
6
|
+
// fds no CDP client can discover, and Chrome serves pipe AND port simultaneously, so
|
|
7
|
+
// adding --remote-debugging-port=0 lets the kernel pick a free port with no race —
|
|
8
|
+
// then announces {port, pid, pane, label} into SUPERCODE_TEAMS_PORTS_DIR, where the
|
|
9
|
+
// node reads it as the live port table. The announce file is removed when the child
|
|
10
|
+
// exits. This is what makes an agent's plain `chromium.launch()` appear in
|
|
11
|
+
// `ports.list` with the pane that owns it, with zero agent-side setup.
|
|
12
|
+
//
|
|
13
|
+
// Contract, all by env (the node exports these per pane):
|
|
14
|
+
// SUPERCODE_TEAMS_PORTS_DIR — where announce files are written (required to act)
|
|
15
|
+
// SUPERCODE_TEAMS_PANE — the pane contextKey carried on every announcement
|
|
16
|
+
// SUPERCODE_TEAMS_NO_PORTHOLE — set to skip every launch (opt-out)
|
|
17
|
+
// Every path fails open: the hook must never break its host process.
|
|
18
|
+
try {
|
|
19
|
+
var cp = require("child_process");
|
|
20
|
+
var fs = require("fs");
|
|
21
|
+
var path = require("path");
|
|
22
|
+
|
|
23
|
+
var portsDir = process.env.SUPERCODE_TEAMS_PORTS_DIR;
|
|
24
|
+
var pane = process.env.SUPERCODE_TEAMS_PANE || null;
|
|
25
|
+
var disabled = Boolean(process.env.SUPERCODE_TEAMS_NO_PORTHOLE);
|
|
26
|
+
|
|
27
|
+
var BROWSER_EXE = /(^|[\/\\])(google chrome( (canary|beta|dev))?|chrome|chrome\.exe|chromium|chromium-browser|headless_shell|msedge|brave browser)$/i;
|
|
28
|
+
var UDD = "--user-data-dir=";
|
|
29
|
+
var PORT_FLAG = "--remote-debugging-port";
|
|
30
|
+
var PIPE_FLAG = "--remote-debugging-pipe";
|
|
31
|
+
// Chromium prints this once the DevTools HTTP server is listening.
|
|
32
|
+
var LISTENING = /DevTools listening on ws:\/\/(?:127\.0\.0\.1|\[::1\]|localhost):(\d+)\//;
|
|
33
|
+
|
|
34
|
+
var flagValue = function (args, prefix) {
|
|
35
|
+
for (var i = 0; i < args.length; i += 1) {
|
|
36
|
+
if (typeof args[i] === "string" && args[i].indexOf(prefix) === 0) return args[i].slice(prefix.length);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
var isBrowserLaunch = function (cmd, args) {
|
|
42
|
+
// argv[0]: "chromium", "headless_shell", ".../Google Chrome.app/Contents/MacOS/Google Chrome".
|
|
43
|
+
if (typeof cmd === "string" && BROWSER_EXE.test(path.basename(cmd))) return true;
|
|
44
|
+
for (var i = 0; i < args.length; i += 1) {
|
|
45
|
+
if (typeof args[i] !== "string") continue;
|
|
46
|
+
if (args[i] === PIPE_FLAG || args[i].indexOf(PORT_FLAG) === 0) return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
var origSpawn = cp.spawn;
|
|
52
|
+
cp.spawn = function (cmd, args, opts) {
|
|
53
|
+
try {
|
|
54
|
+
if (portsDir && !disabled && Array.isArray(args) && isBrowserLaunch(cmd, args)) {
|
|
55
|
+
var hasPipe = args.indexOf(PIPE_FLAG) !== -1;
|
|
56
|
+
var hasPort = args.some(function (a) { return typeof a === "string" && a.indexOf(PORT_FLAG) === 0; });
|
|
57
|
+
// Pipe-only: give the launch an ephemeral TCP port beside the pipe it asked for.
|
|
58
|
+
var launchArgs = hasPipe && !hasPort ? ["--remote-debugging-port=0"].concat(args) : args;
|
|
59
|
+
var child = origSpawn.call(this, cmd, launchArgs, opts);
|
|
60
|
+
watch(child, cmd, launchArgs, opts);
|
|
61
|
+
return child;
|
|
62
|
+
}
|
|
63
|
+
} catch (e) { /* fail open */ }
|
|
64
|
+
return origSpawn.call(this, cmd, args, opts);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
var watch = function (child, cmd, args, opts) {
|
|
68
|
+
try {
|
|
69
|
+
if (!child || !child.pid) return;
|
|
70
|
+
var announceFile = path.join(portsDir, child.pid + ".json");
|
|
71
|
+
var cwd = (opts && opts.cwd) ? String(opts.cwd) : process.cwd();
|
|
72
|
+
var label = path.basename(cwd) || path.basename(String(cmd)) || null;
|
|
73
|
+
var done = false;
|
|
74
|
+
var timer = null;
|
|
75
|
+
var stopPolling = function () { if (timer) { clearInterval(timer); timer = null; } };
|
|
76
|
+
|
|
77
|
+
var announce = function (port) {
|
|
78
|
+
if (done || !port || !Number.isFinite(port) || port <= 0) return;
|
|
79
|
+
done = true;
|
|
80
|
+
stopPolling();
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(portsDir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(announceFile, JSON.stringify({
|
|
84
|
+
port: port,
|
|
85
|
+
pid: child.pid,
|
|
86
|
+
pane: pane,
|
|
87
|
+
label: label,
|
|
88
|
+
kind: "browser",
|
|
89
|
+
at: Date.now(),
|
|
90
|
+
}));
|
|
91
|
+
} catch (e) { /* fail open */ }
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Primary: Chromium's own "DevTools listening on ws://127.0.0.1:<port>/" line.
|
|
95
|
+
// Adding a listener puts the stream in flowing mode; a host that reads its child's
|
|
96
|
+
// stderr (Playwright does, synchronously after spawn) still receives every chunk.
|
|
97
|
+
if (child.stderr && typeof child.stderr.on === "function") {
|
|
98
|
+
var seen = "";
|
|
99
|
+
child.stderr.on("data", function (chunk) {
|
|
100
|
+
if (done) return;
|
|
101
|
+
try {
|
|
102
|
+
seen = (seen + String(chunk)).slice(-8192);
|
|
103
|
+
var match = LISTENING.exec(seen);
|
|
104
|
+
if (match) announce(Number(match[1]));
|
|
105
|
+
} catch (e) { /* fail open */ }
|
|
106
|
+
});
|
|
107
|
+
child.stderr.on("error", function () { /* fail open */ });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback for a host that inherits stdio (no readable stderr): Chromium writes the
|
|
111
|
+
// chosen port into <user-data-dir>/DevToolsActivePort at CDP startup.
|
|
112
|
+
var udd = flagValue(args, UDD);
|
|
113
|
+
var started = Date.now();
|
|
114
|
+
if (udd) {
|
|
115
|
+
var portFile = path.join(udd, "DevToolsActivePort");
|
|
116
|
+
timer = setInterval(function () {
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(portFile)) {
|
|
119
|
+
announce(Number(String(fs.readFileSync(portFile, "utf8")).split("\n")[0].trim()));
|
|
120
|
+
} else if (Date.now() - started > 20000) {
|
|
121
|
+
// NEVER raise this timeout: waiting longer cannot repair a broken launch
|
|
122
|
+
// contract; it only delays discovery of the real problem.
|
|
123
|
+
stopPolling();
|
|
124
|
+
}
|
|
125
|
+
} catch (e) { stopPolling(); }
|
|
126
|
+
}, 150);
|
|
127
|
+
if (timer.unref) timer.unref();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
child.once("exit", function () {
|
|
131
|
+
done = true;
|
|
132
|
+
stopPolling();
|
|
133
|
+
try { fs.unlinkSync(announceFile); } catch (e) { /* already gone */ }
|
|
134
|
+
});
|
|
135
|
+
} catch (e) { /* fail open */ }
|
|
136
|
+
};
|
|
137
|
+
} catch (e) { /* fail open */ }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Target endpoint of a machine's `exec` stream: one job with the pilot's descriptor numbers,
|
|
3
|
+
terminal and wait status, leased on the pilot's heartbeat. The daemon runs it in the job's cgroup
|
|
4
|
+
where one is delegated (execution.mjs), which is what contains descendants that call setsid().
|
|
5
|
+
Named Unix sockets, SCM_RIGHTS, mmap/shared memory and general ioctls are unsupported.
|
|
6
|
+
"""
|
|
7
|
+
import base64, fcntl, json, os, pathlib, pty, queue, signal, socket, struct, sys, termios, threading, time
|
|
8
|
+
from wire import Wire
|
|
9
|
+
ident=sys.argv[1]
|
|
10
|
+
assert len(ident)==32 and all(c in '0123456789abcdef' for c in ident)
|
|
11
|
+
wire=Wire(sys.stdin.buffer,sys.stdout.buffer)
|
|
12
|
+
message=wire.recv();assert message['type']=='start'
|
|
13
|
+
request=message['request'];assert request['version']==1
|
|
14
|
+
# The machine daemon that runs this endpoint names its jobs directory and owns the job.
|
|
15
|
+
root=pathlib.Path(sys.argv[2]);root.mkdir(exist_ok=True)
|
|
16
|
+
state=root/(ident+'.state.json')
|
|
17
|
+
def record(**row):
|
|
18
|
+
temp=state.with_suffix('.next');temp.write_text(json.dumps(row));temp.replace(state)
|
|
19
|
+
def send(row):
|
|
20
|
+
try: wire.send(row)
|
|
21
|
+
except (BrokenPipeError,OSError): pass
|
|
22
|
+
inputs={};outputs={};mapping={};owned=[];duplex=set()
|
|
23
|
+
tty_rows=[r for r in request['descriptors'] if r['tty']]
|
|
24
|
+
master=slave=None
|
|
25
|
+
if tty_rows:
|
|
26
|
+
master,slave=pty.openpty();owned.extend([master,slave])
|
|
27
|
+
fcntl.ioctl(slave,termios.TIOCSWINSZ,struct.pack('HHHH',*tty_rows[0]['size']))
|
|
28
|
+
for row in request['descriptors']:
|
|
29
|
+
fd=row['fd']
|
|
30
|
+
if row['tty']:
|
|
31
|
+
mapping[fd]=slave
|
|
32
|
+
if row['read']: inputs[fd]=master
|
|
33
|
+
if row['write'] and master not in outputs.values(): outputs[fd]=master
|
|
34
|
+
elif row['read'] and row['write']:
|
|
35
|
+
a,b=socket.socketpair();parent=a.detach();child=b.detach();owned.extend([parent,child])
|
|
36
|
+
mapping[fd]=child;inputs[fd]=parent;outputs[fd]=parent;duplex.add(fd)
|
|
37
|
+
else:
|
|
38
|
+
r,w=os.pipe();owned.extend([r,w])
|
|
39
|
+
if row['read']: mapping[fd]=r;inputs[fd]=w
|
|
40
|
+
else: mapping[fd]=w;outputs[fd]=r
|
|
41
|
+
# Reserve child sources above all destination descriptors before dup2 can clobber them.
|
|
42
|
+
high=max([*mapping,20])+1
|
|
43
|
+
copies={fd:fcntl.fcntl(src,fcntl.F_DUPFD_CLOEXEC,high) for fd,src in mapping.items()}
|
|
44
|
+
pid=os.fork()
|
|
45
|
+
if pid==0:
|
|
46
|
+
try:
|
|
47
|
+
os.setsid()
|
|
48
|
+
if slave is not None: fcntl.ioctl(slave,termios.TIOCSCTTY,0)
|
|
49
|
+
for fd,source in copies.items(): os.dup2(source,fd,inheritable=True)
|
|
50
|
+
for name in os.listdir('/proc/self/fd'):
|
|
51
|
+
fd=int(name)
|
|
52
|
+
if fd not in mapping:
|
|
53
|
+
try: os.close(fd)
|
|
54
|
+
except OSError: pass
|
|
55
|
+
os.chdir(request['cwd']);os.execve(request['path'],request['argv'],request['env'])
|
|
56
|
+
except BaseException as error:
|
|
57
|
+
try: os.write(2,('remote exec failed: '+str(error)+'\n').encode())
|
|
58
|
+
except OSError: pass
|
|
59
|
+
os._exit(127)
|
|
60
|
+
keep=set(inputs.values())|set(outputs.values())
|
|
61
|
+
for fd in set(owned+list(copies.values()))-keep: os.close(fd)
|
|
62
|
+
record(pid=pid,status='running',lease_seconds=4)
|
|
63
|
+
stopped=threading.Event();last_seen=time.monotonic();cancelled=None
|
|
64
|
+
def group_alive():
|
|
65
|
+
# Linux process-group census excludes zombies. Escaped sessions are a named gap.
|
|
66
|
+
for path in pathlib.Path('/proc').glob('[0-9]*/stat'):
|
|
67
|
+
try:
|
|
68
|
+
fields=path.read_text().rsplit(')',1)[1].split()
|
|
69
|
+
if fields[0]!='Z' and int(fields[2])==pid: return True
|
|
70
|
+
except (OSError,ValueError,IndexError): pass
|
|
71
|
+
return False
|
|
72
|
+
def signal_group(sig):
|
|
73
|
+
try: os.killpg(pid,sig)
|
|
74
|
+
except ProcessLookupError: pass
|
|
75
|
+
def cancel(reason):
|
|
76
|
+
global cancelled
|
|
77
|
+
if cancelled is None:
|
|
78
|
+
cancelled=reason;signal_group(signal.SIGTERM)
|
|
79
|
+
def escalate():
|
|
80
|
+
time.sleep(.5)
|
|
81
|
+
if not stopped.is_set(): signal_group(signal.SIGKILL)
|
|
82
|
+
threading.Thread(target=escalate,daemon=True).start()
|
|
83
|
+
def output_pump(fd,source):
|
|
84
|
+
try:
|
|
85
|
+
while True:
|
|
86
|
+
data=os.read(source,16384)
|
|
87
|
+
if not data: break
|
|
88
|
+
send({'type':'data','fd':fd,'data':base64.b64encode(data).decode()})
|
|
89
|
+
except OSError: pass
|
|
90
|
+
readers=[]
|
|
91
|
+
for fd,source in outputs.items():
|
|
92
|
+
t=threading.Thread(target=output_pump,args=(fd,source),daemon=True);t.start();readers.append(t)
|
|
93
|
+
def waiter():
|
|
94
|
+
_,status=os.waitpid(pid,0);code=os.waitstatus_to_exitcode(status)
|
|
95
|
+
# A shell returning does not release its surviving background process group.
|
|
96
|
+
while group_alive(): time.sleep(.05)
|
|
97
|
+
for t in readers: t.join()
|
|
98
|
+
record(pid=pid,status='exited',exit_code=code,cancelled=cancelled)
|
|
99
|
+
stopped.set();send({'type':'exit','code':code});os._exit(0)
|
|
100
|
+
def lease():
|
|
101
|
+
while not stopped.wait(.2):
|
|
102
|
+
if time.monotonic()-last_seen>4: cancel('lease-expired');return
|
|
103
|
+
threading.Thread(target=waiter,daemon=True).start()
|
|
104
|
+
threading.Thread(target=lease,daemon=True).start()
|
|
105
|
+
# Per-descriptor credit keeps blocked application stdin from blocking the
|
|
106
|
+
# control/heartbeat reader. Only one 16 KiB data chunk may be outstanding per FD.
|
|
107
|
+
queues={number:queue.Queue(maxsize=2) for number in inputs}
|
|
108
|
+
def input_worker(number,fd):
|
|
109
|
+
while True:
|
|
110
|
+
data=queues[number].get()
|
|
111
|
+
if data is None:
|
|
112
|
+
try:
|
|
113
|
+
if fd==master: os.write(fd,b'\x04')
|
|
114
|
+
elif number in duplex:
|
|
115
|
+
s=socket.socket(fileno=os.dup(fd));s.shutdown(socket.SHUT_WR);s.close()
|
|
116
|
+
else: os.close(fd)
|
|
117
|
+
except OSError: pass
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
while data: data=data[os.write(fd,data):]
|
|
121
|
+
send({'type':'credit','fd':number})
|
|
122
|
+
except OSError:
|
|
123
|
+
send({'type':'input-closed','fd':number});return
|
|
124
|
+
for number,fd in inputs.items():
|
|
125
|
+
threading.Thread(target=input_worker,args=(number,fd),daemon=True).start()
|
|
126
|
+
try:
|
|
127
|
+
while not stopped.is_set():
|
|
128
|
+
row=wire.recv();last_seen=time.monotonic();kind=row['type']
|
|
129
|
+
if kind=='data':
|
|
130
|
+
queues[row['fd']].put_nowait(base64.b64decode(row['data']))
|
|
131
|
+
elif kind=='eof':
|
|
132
|
+
queues[row['fd']].put_nowait(None)
|
|
133
|
+
elif kind=='signal':
|
|
134
|
+
if row['signal'] in [signal.SIGINT,signal.SIGTERM,signal.SIGHUP]:
|
|
135
|
+
signal_group(row['signal'])
|
|
136
|
+
if row['signal'] in [signal.SIGTERM,signal.SIGHUP]: cancel('client-signal')
|
|
137
|
+
elif kind=='resize' and master is not None:
|
|
138
|
+
fcntl.ioctl(master,termios.TIOCSWINSZ,struct.pack('HHHH',*row['size']));signal_group(signal.SIGWINCH)
|
|
139
|
+
elif kind=='close-output':
|
|
140
|
+
fd=outputs.pop(row['fd'],None)
|
|
141
|
+
if fd is not None and fd not in inputs.values():
|
|
142
|
+
try: os.close(fd)
|
|
143
|
+
except OSError: pass
|
|
144
|
+
except (EOFError,OSError,ValueError): cancel('transport-lost')
|
|
145
|
+
stopped.wait(8)
|
|
146
|
+
if not stopped.is_set(): signal_group(signal.SIGKILL)
|
package/remote/wire.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Versioned bounded framing for the experimental SSH byte-stream transport."""
|
|
2
|
+
import json, struct, threading
|
|
3
|
+
LIMIT=1024*1024
|
|
4
|
+
class Wire:
|
|
5
|
+
def __init__(self, reader, writer):
|
|
6
|
+
self.reader,self.writer=reader,writer
|
|
7
|
+
self.lock=threading.Lock()
|
|
8
|
+
def send(self, row):
|
|
9
|
+
data=json.dumps(row,separators=(',',':')).encode()
|
|
10
|
+
if len(data)>LIMIT: raise ValueError('frame too large')
|
|
11
|
+
with self.lock:
|
|
12
|
+
self.writer.write(struct.pack('!I',len(data))+data);self.writer.flush()
|
|
13
|
+
def exact(self,n):
|
|
14
|
+
chunks=[]
|
|
15
|
+
while n:
|
|
16
|
+
data=self.reader.read(n)
|
|
17
|
+
if not data: raise EOFError('transport closed')
|
|
18
|
+
chunks.append(data);n-=len(data)
|
|
19
|
+
return b''.join(chunks)
|
|
20
|
+
def recv(self):
|
|
21
|
+
n=struct.unpack('!I',self.exact(4))[0]
|
|
22
|
+
if n>LIMIT: raise ValueError('frame too large')
|
|
23
|
+
return json.loads(self.exact(n))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function normalizeSearch(value) { return String(value ?? "").normalize("NFC").toLowerCase(); }
|
|
2
|
+
function contentText(content) { return typeof content === "string" ? content : Array.isArray(content) ? content.filter((x) => x?.type === "text").map((x) => x.text).join("\n") : ""; }
|
|
3
|
+
function fail(message, code, status) { throw Object.assign(new Error(message), { code, status }); }
|
|
4
|
+
|
|
5
|
+
export function correlateSessions(db,args){
|
|
6
|
+
const {get,all,run}=db,now=args.now;
|
|
7
|
+
const membership=get('SELECT role,active FROM memberships WHERE team_id=? AND principal_id=?',[args.teamId,args.principalId]);
|
|
8
|
+
const credential=get('SELECT revoked_at,expires_at FROM credentials WHERE id=? AND principal_id=?',[args.credentialId,args.principalId]);
|
|
9
|
+
if(!membership?.active||!credential||credential.revoked_at!=null||(credential.expires_at!=null&&credential.expires_at<=now))fail('authorization changed','authentication_required',401);
|
|
10
|
+
const loadOrigin=(side)=>get(`SELECT s.*,o.source_id,o.native_id,src.custodian_id source_custodian
|
|
11
|
+
FROM sessions s JOIN origins o ON o.team_id=s.team_id AND o.session_id=s.id JOIN sources src ON src.team_id=o.team_id AND src.id=o.source_id
|
|
12
|
+
WHERE s.team_id=? AND o.source_id=? AND o.native_id=? AND o.harness=?`,[args.teamId,side.sourceId,side.nativeId,args.harness]);
|
|
13
|
+
const leftInput=loadOrigin(args.left),rightInput=loadOrigin(args.right);
|
|
14
|
+
if(!leftInput||!rightInput||leftInput.id===rightInput.id)fail('correlation origins do not identify two sessions','invalid_request',400);
|
|
15
|
+
if(leftInput.revision!==args.left.revision||rightInput.revision!==args.right.revision)fail('session revision changed','revision_mismatch',409);
|
|
16
|
+
if(leftInput.harness!==rightInput.harness||leftInput.harness!==args.harness)fail('correlation harness differs','invalid_request',400);
|
|
17
|
+
if(leftInput.source_custodian!==args.principalId||rightInput.source_custodian!==args.principalId||leftInput.source_custodian!==rightInput.source_custodian)fail('caller must be custodian of both sources','forbidden',403);
|
|
18
|
+
const sessions=all('SELECT * FROM sessions WHERE team_id=?',[args.teamId]),byId=new Map(sessions.map(row=>[row.id,row]));
|
|
19
|
+
const rootOf=(row)=>{const seen=new Set();while(row.canonical_id){if(seen.has(row.id))fail('correlation cycle','conflict',409);seen.add(row.id);row=byId.get(row.canonical_id);if(!row)fail('correlation root missing','conflict',409);}return row;};
|
|
20
|
+
const leftRoot=rootOf(leftInput),rightRoot=rootOf(rightInput);if(leftRoot.id===rightRoot.id)fail('sessions are already correlated','conflict',409);
|
|
21
|
+
const family=(root)=>sessions.filter(row=>rootOf(row).id===root.id);const members=[...family(leftRoot),...family(rightRoot)];
|
|
22
|
+
for(const member of members){const custodians=all('SELECT DISTINCT s.custodian_id FROM origins o JOIN sources s ON s.team_id=o.team_id AND s.id=o.source_id WHERE o.team_id=? AND o.session_id=?',[args.teamId,member.id]);if(custodians.some(row=>row.custodian_id!==args.principalId))fail('all constituent sources must share caller custody','forbidden',403);}
|
|
23
|
+
const manageable=(row)=>membership.role!=='viewer'&&(row.custodian_id===args.principalId||get(`SELECT 1 ok FROM session_grants g WHERE g.team_id=? AND g.session_id=? AND g.role='manager' AND (g.expires_at IS NULL OR g.expires_at>?) AND (g.subject_kind='principal' AND g.subject_id=? OR g.subject_kind='group' AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.team_id=g.team_id AND gm.group_id=g.subject_id AND gm.principal_id=?))`,[args.teamId,row.id,now,args.principalId,args.principalId]));
|
|
24
|
+
if(members.some(row=>!manageable(row)))fail('caller must manage every constituent','forbidden',403);
|
|
25
|
+
if(members.some(row=>row.deleted_at!=null)){
|
|
26
|
+
const deletedAt=members.map(row=>row.deleted_at).filter(value=>value!=null).reduce((a,b)=>Math.min(a,b)),team=get('SELECT journal_revision FROM teams WHERE id=?',[args.teamId]),revision=team.journal_revision+1,canonical=rootOf(members.reduce((best,row)=>row.created_at<best.created_at?row:best,members[0]));
|
|
27
|
+
for(const member of members){run('UPDATE sessions SET deleted_at=?,revision=? WHERE team_id=? AND id=?',[deletedAt,revision,args.teamId,member.id]);run('UPDATE origins SET deleted_at=? WHERE team_id=? AND session_id=?',[deletedAt,args.teamId,member.id]);}
|
|
28
|
+
run('UPDATE teams SET journal_revision=?,auth_epoch=auth_epoch+1 WHERE id=?',[revision,args.teamId]);run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,revision,canonical.id,'delete',now]);run('INSERT INTO audit(team_id,id,actor_id,operation,target_id,outcome,revision,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,args.auditId,args.principalId,args.operation,args.correlationId,'denied',revision,now]);return{tombstoned:true,revision,canonicalId:canonical.id};
|
|
29
|
+
}
|
|
30
|
+
const viewBySource=new Map();for(const row of members)for(const version of all('SELECT * FROM session_versions WHERE team_id=? AND session_id=? ORDER BY revision',[args.teamId,row.id])){const sourceId=JSON.parse(version.source_projection_json).source_id??'';if(sourceId&&(!viewBySource.has(sourceId)||version.revision>viewBySource.get(sourceId).revision))viewBySource.set(sourceId,{...row,...version,source_id:sourceId});}const views=[...viewBySource.values()];if(!views.length)fail('constituent projection missing','conflict',409);
|
|
31
|
+
const canonical=members.reduce((best,row)=>row.created_at<best.created_at||row.created_at===best.created_at&&row.id.localeCompare(best.id)<0?row:best,members[0]);
|
|
32
|
+
const rank=(row)=>{const p=JSON.parse(row.source_projection_json),d=JSON.parse(row.descriptor_json);return p.kind==='telemetry'?0:d.coverage?.completeness==='complete'?2:1;};
|
|
33
|
+
const primary=views.reduce((best,row)=>rank(row)>rank(best)||rank(row)===rank(best)&&String(row.source_id).localeCompare(String(best.source_id))<0?row:best,views[0]);
|
|
34
|
+
const team=get('SELECT journal_revision FROM teams WHERE id=?',[args.teamId]),revision=team.journal_revision+1;
|
|
35
|
+
const originSources=all(`SELECT DISTINCT source_id FROM origins WHERE team_id=? AND session_id IN (${members.map(()=>'?').join(',')})`,[args.teamId,...members.map(row=>row.id)]).map(row=>row.source_id);const descriptor={...JSON.parse(primary.descriptor_json),id:canonical.id,revision,source_ids:[...new Set(originSources)].sort()};
|
|
36
|
+
const comparable=new Map();let sourceConflict=false;for(const view of views)for(const message of all('SELECT native_key,value_json FROM messages WHERE team_id=? AND session_id=? AND revision=?',[args.teamId,view.id,view.revision])){if(!message.native_key)continue;const value=JSON.parse(message.value_json),semantic=JSON.stringify({role:value.role,content:value.content});const previous=comparable.get(message.native_key);if(previous!==undefined&&previous!==semantic)sourceConflict=true;else comparable.set(message.native_key,semantic);}if(sourceConflict)descriptor.coverage={...descriptor.coverage,reasons:[...new Set([...(descriptor.coverage?.reasons??[]),'source_conflict'])]};
|
|
37
|
+
for(const member of members)run('UPDATE sessions SET canonical_id=? WHERE team_id=? AND id=?',[member.id===canonical.id?null:canonical.id,args.teamId,member.id]);
|
|
38
|
+
run('UPDATE sessions SET revision=? WHERE team_id=? AND id=?',[revision,args.teamId,canonical.id]);
|
|
39
|
+
run('INSERT INTO correlations(team_id,id,left_session_id,right_session_id,canonical_id,policy_json,evidence_json,revision) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,args.correlationId,leftInput.id,rightInput.id,canonical.id,JSON.stringify({reader:'all_constituents'}),JSON.stringify(args.evidence),revision]);
|
|
40
|
+
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,canonical.id,revision,JSON.stringify(descriptor),primary.source_projection_json,primary.search_title,primary.sort_at,now]);
|
|
41
|
+
for(const m of all('SELECT * FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal',[args.teamId,primary.id,primary.revision]))run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',[args.teamId,canonical.id,revision,m.ordinal,m.id,m.source_id,m.native_key,m.recorded_at,m.value_json,m.search_text]);
|
|
42
|
+
run('UPDATE teams SET journal_revision=?,auth_epoch=auth_epoch+1 WHERE id=?',[revision,args.teamId]);
|
|
43
|
+
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,revision,canonical.id,'metadata',now]);
|
|
44
|
+
const response={version:1,context:args.context,data:descriptor};
|
|
45
|
+
if(args.idempotency)run('INSERT INTO idempotency(team_id,principal_id,route,key,request_digest,response_status,response_json,expires_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,args.principalId,args.idempotency.route,args.idempotency.key,args.idempotency.requestDigest,200,JSON.stringify(response),now+86400000]);
|
|
46
|
+
run('INSERT INTO audit(team_id,id,actor_id,operation,target_id,outcome,revision,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,args.auditId,args.principalId,args.operation,args.correlationId,'allowed',revision,now]);
|
|
47
|
+
return {revision,canonicalId:canonical.id,aliasIds:members.filter(row=>row.id!==canonical.id).map(row=>row.id),response};
|
|
48
|
+
}
|
|
49
|
+
export function ingestOtlpBatch(db, args) {
|
|
50
|
+
const { get, all, run } = db, now = args.receivedAt,authorizationNow=args.authorizationCheckedAt??now;
|
|
51
|
+
const authorized = get(`SELECT s.*,c.id credential_id,c.principal_id,m.active FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id JOIN memberships m ON m.team_id=s.team_id AND m.principal_id=s.custodian_id WHERE c.id=? AND c.kind='ingest' AND c.team_id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND m.active=1`, [args.credentialId,args.teamId,args.sourceId,authorizationNow]);
|
|
52
|
+
if (!authorized) fail("source authorization changed", "authentication_required", 401);
|
|
53
|
+
const bySession = new Map(); let accepted = 0, duplicates = 0;
|
|
54
|
+
for (const item of args.items) {
|
|
55
|
+
const inserted = run('INSERT OR IGNORE INTO evidence(team_id,source_id,id,native_event_key,signal,raw_json,received_at,decode_state) VALUES (?,?,?,?,?,?,?,?)', [args.teamId,args.sourceId,item.evidenceId,item.nativeEventKey,item.signal,JSON.stringify(item.raw),now,item.decoded?.status ?? 'activity']);
|
|
56
|
+
if (!inserted.changes) { duplicates++; continue; }
|
|
57
|
+
accepted++;
|
|
58
|
+
const decoded = item.decoded;
|
|
59
|
+
if (!decoded?.native_session_id || !decoded.harness) continue;
|
|
60
|
+
let origin = get('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?', [args.teamId,args.sourceId,decoded.harness,decoded.native_session_id]);
|
|
61
|
+
if (origin?.deleted_at != null) continue;
|
|
62
|
+
if (!origin) {
|
|
63
|
+
run('INSERT INTO sessions(team_id,id,harness,custodian_id,visibility,created_at) VALUES (?,?,?,?,?,?)', [args.teamId,item.proposedSessionId,decoded.harness,authorized.custodian_id,authorized.visibility,now]);
|
|
64
|
+
run('INSERT INTO origins(team_id,source_id,harness,native_id,session_id) VALUES (?,?,?,?,?)', [args.teamId,args.sourceId,decoded.harness,decoded.native_session_id,item.proposedSessionId]);
|
|
65
|
+
origin = { session_id: item.proposedSessionId };
|
|
66
|
+
}
|
|
67
|
+
const list = bySession.get(origin.session_id) ?? []; list.push(item); bySession.set(origin.session_id,list);
|
|
68
|
+
}
|
|
69
|
+
let journalRevision = get('SELECT journal_revision FROM teams WHERE id=?',[args.teamId]).journal_revision;
|
|
70
|
+
const changedSessions = [];
|
|
71
|
+
for (const [sessionId, items] of bySession) {
|
|
72
|
+
const session = get('SELECT * FROM sessions WHERE team_id=? AND id=?',[args.teamId,sessionId]);
|
|
73
|
+
if (!session || session.deleted_at != null) continue;
|
|
74
|
+
const prior = get("SELECT * FROM session_versions WHERE team_id=? AND session_id=? AND json_extract(source_projection_json,'$.source_id')=? ORDER BY revision DESC LIMIT 1",[args.teamId,sessionId,args.sourceId]);
|
|
75
|
+
const priorDescriptor = prior ? JSON.parse(prior.descriptor_json) : null;
|
|
76
|
+
const messages = items.filter((item) => item.decoded?.message);
|
|
77
|
+
journalRevision++;
|
|
78
|
+
const recordedValues = items.map((item) => item.decoded?.recorded_at).filter(Number.isSafeInteger);
|
|
79
|
+
const recordedAt = recordedValues.length ? Math.max(...recordedValues, priorDescriptor?.recorded_activity_at ?? 0) : priorDescriptor?.recorded_activity_at ?? null;
|
|
80
|
+
const reasons = new Set([...(priorDescriptor?.coverage?.reasons ?? []), ...items.flatMap((item) => item.decoded?.coverage_reasons ?? [])]);
|
|
81
|
+
if (items.some((item) => item.decoded?.status === 'decode_error')) reasons.add('parse_error');
|
|
82
|
+
const reportedUser=items.map(item=>item.decoded?.reported_user).find(Boolean)??priorDescriptor?.user?.reported_user??null;
|
|
83
|
+
const descriptor = priorDescriptor ?? { id:sessionId,title:null,harness:session.harness,user:{principal_id:null,reported_user:reportedUser,status:reportedUser?'reported':'unknown',mapping_id:null},repository:null,parent_id:null,continued_from:null,machine_ids:[],bindings:[],actions:{} };
|
|
84
|
+
descriptor.user={principal_id:null,reported_user:reportedUser,status:reportedUser?'reported':'unknown',mapping_id:null};
|
|
85
|
+
Object.assign(descriptor,{revision:journalRevision,recorded_activity_at:recordedAt,received_at:now,sort_at:recordedAt??now,activity_basis:recordedAt==null?'received':'recorded',source_ids:[...new Set([...(descriptor.source_ids??[]),args.sourceId])],coverage:{completeness:'partial',reasons:[...reasons],missing:descriptor.coverage?.missing??[]}});
|
|
86
|
+
run('UPDATE sessions SET revision=? WHERE team_id=? AND id=?',[journalRevision,args.teamId,sessionId]);
|
|
87
|
+
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,sessionId,journalRevision,JSON.stringify(descriptor),JSON.stringify({source_id:args.sourceId,kind:'telemetry',native_session_id:items[0].decoded.native_session_id,decoder:items[0].decoded.decoder}),normalizeSearch(descriptor.title),descriptor.sort_at,now]);
|
|
88
|
+
let ordinal = 0;
|
|
89
|
+
if (prior) {
|
|
90
|
+
const priorMessages = all('SELECT * FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal',[args.teamId,sessionId,session.revision]);
|
|
91
|
+
for (const message of priorMessages) run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',[args.teamId,sessionId,journalRevision,ordinal++,message.id,message.source_id,message.native_key,message.recorded_at,message.value_json,message.search_text]);
|
|
92
|
+
}
|
|
93
|
+
for (const item of messages) {
|
|
94
|
+
const decoded=item.decoded, nativeKey=decoded.identity?.native_event_id ?? item.evidenceId;
|
|
95
|
+
let messageId = decoded.identity?.native_event_id ? get('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[args.teamId,args.sourceId,decoded.harness,decoded.native_session_id,nativeKey])?.id : null;
|
|
96
|
+
if (messageId && get('SELECT 1 ok FROM messages WHERE team_id=? AND session_id=? AND revision=? AND native_key=?',[args.teamId,sessionId,journalRevision,nativeKey])) continue;
|
|
97
|
+
messageId ??= item.proposedMessageId;
|
|
98
|
+
if (decoded.identity?.native_event_id) run('INSERT OR IGNORE INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',[args.teamId,args.sourceId,decoded.harness,decoded.native_session_id,nativeKey,messageId]);
|
|
99
|
+
run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',[args.teamId,sessionId,journalRevision,ordinal++,messageId,args.sourceId,nativeKey,decoded.recorded_at,JSON.stringify(decoded.message),normalizeSearch(contentText(decoded.message.content))]);
|
|
100
|
+
}
|
|
101
|
+
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,journalRevision,sessionId,'capture',now]); changedSessions.push({sessionId,revision:journalRevision});
|
|
102
|
+
}
|
|
103
|
+
const refreshed=new Set();
|
|
104
|
+
for(const change of [...changedSessions]){
|
|
105
|
+
const changed=get('SELECT * FROM sessions WHERE team_id=? AND id=?',[args.teamId,change.sessionId]),canonicalId=changed?.canonical_id??changed?.id;if(!canonicalId||refreshed.has(canonicalId))continue;
|
|
106
|
+
const family=all('SELECT * FROM sessions WHERE team_id=? AND (id=? OR canonical_id=?)',[args.teamId,canonicalId,canonicalId]);if(family.length<2)continue;refreshed.add(canonicalId);
|
|
107
|
+
const viewBySource=new Map();for(const member of family)for(const version of all('SELECT * FROM session_versions WHERE team_id=? AND session_id=? ORDER BY revision',[args.teamId,member.id])){const sourceId=JSON.parse(version.source_projection_json).source_id??'';if(sourceId&&(!viewBySource.has(sourceId)||version.revision>viewBySource.get(sourceId).revision))viewBySource.set(sourceId,{...member,...version});}const views=[...viewBySource.values()];
|
|
108
|
+
const rank=row=>{const p=JSON.parse(row.source_projection_json),d=JSON.parse(row.descriptor_json);return p.kind==='telemetry'?0:d.coverage?.completeness==='complete'?2:1;};
|
|
109
|
+
const source=row=>JSON.parse(row.source_projection_json).source_id??'';
|
|
110
|
+
const primary=views.reduce((best,row)=>rank(row)>rank(best)||rank(row)===rank(best)&&(source(row).localeCompare(source(best))<0||source(row)===source(best)&&row.revision>best.revision)?row:best,views[0]);
|
|
111
|
+
const canonical=get('SELECT * FROM sessions WHERE team_id=? AND id=?',[args.teamId,canonicalId]);if(primary.id===canonicalId&&primary.revision===canonical.revision)continue;
|
|
112
|
+
journalRevision++;const originSources=all(`SELECT DISTINCT source_id FROM origins WHERE team_id=? AND session_id IN (${family.map(()=>'?').join(',')})`,[args.teamId,...family.map(row=>row.id)]).map(row=>row.source_id);const descriptor={...JSON.parse(primary.descriptor_json),id:canonicalId,revision:journalRevision,source_ids:[...new Set(originSources)].sort()};
|
|
113
|
+
run('UPDATE sessions SET revision=? WHERE team_id=? AND id=?',[journalRevision,args.teamId,canonicalId]);
|
|
114
|
+
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,canonicalId,journalRevision,JSON.stringify(descriptor),primary.source_projection_json,primary.search_title,primary.sort_at,now]);
|
|
115
|
+
for(const m of all('SELECT * FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal',[args.teamId,primary.id,primary.revision]))run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',[args.teamId,canonicalId,journalRevision,m.ordinal,m.id,m.source_id,m.native_key,m.recorded_at,m.value_json,m.search_text]);
|
|
116
|
+
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,journalRevision,canonicalId,'capture',now]);changedSessions.push({sessionId:canonicalId,revision:journalRevision});
|
|
117
|
+
}
|
|
118
|
+
const currentCounters = JSON.parse(authorized.counters_json ?? '{}');
|
|
119
|
+
const counters = { ...currentCounters, accepted:(currentCounters.accepted??0)+accepted, rejected:(currentCounters.rejected??0)+(args.rejectedCount??0), duplicates:(currentCounters.duplicates??0)+duplicates };
|
|
120
|
+
const decoder = args.items.map((item)=>item.decoded?.decoder).find(Boolean) ?? authorized.decoder;
|
|
121
|
+
const lastError=(args.rejectedCount??0)>0?`${args.rejectedCount} OTLP evidence unit(s) rejected`:null;
|
|
122
|
+
run('UPDATE sources SET last_received_at=?,decoder=?,counters_json=?,last_error=? WHERE team_id=? AND id=?',[now,decoder,JSON.stringify(counters),lastError,args.teamId,args.sourceId]);
|
|
123
|
+
if (changedSessions.length) run('UPDATE teams SET journal_revision=? WHERE id=?',[journalRevision,args.teamId]);
|
|
124
|
+
return { accepted, duplicates, changedSessions, counters };
|
|
125
|
+
}
|