@volter/supercode-teams 0.2.170 → 0.2.172
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/package.json +1 -1
- package/server/catalog.mjs +14 -0
- package/server/event-routes.mjs +7 -3
- package/server/harness-routes.mjs +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.172",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"license": "MIT",
|
package/server/catalog.mjs
CHANGED
|
@@ -5,3 +5,17 @@ export { cursorToken, cursorHash };
|
|
|
5
5
|
export const normalizeSearch = text => String(text).normalize('NFC').toLowerCase();
|
|
6
6
|
export function descriptor(row, auth, grants=[]) { const projection=JSON.parse(row.descriptor_json); projection.actions ??= {}; const read=canRead(auth,row,grants); for(const action of ['read','follow','export_text','export_native','manage','attach_observe','interact','approve','interrupt','terminate','continue']) projection.actions[action] ??={allowed:action==='read'||action==='follow'||action==='export_text'?read:false,reason:read?(action==='read'||action==='follow'||action==='export_text'?null:'unsupported'):'forbidden'}; return projection; }
|
|
7
7
|
export function textFromContent(content) { if(typeof content==='string')return content; if(!Array.isArray(content))return ''; return content.filter(x=>x&&typeof x==='object'&&x.type==='text'&&typeof x.text==='string').map(x=>x.text).join('\n'); }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The native bindings sessions have now (contracts: Descriptor.bindings), by session id: each one a machine reported in
|
|
11
|
+
* its latest heartbeat, from the incarnation it runs as, on a machine still enrolled and seen within the window attach
|
|
12
|
+
* uses (effects.mjs), so a read offers only bindings attach would accept. Only the contract's fields are served. The
|
|
13
|
+
* captured descriptor never holds them; a heartbeat replaces them, so every read takes them fresh.
|
|
14
|
+
*/
|
|
15
|
+
const BINDING_FIELDS=['id','machine_id','incarnation_id','native_session_id','native_runtime_id','harness','state','observed_at','native_capabilities'];
|
|
16
|
+
export async function liveBindings(api,teamId,sessionId=null){
|
|
17
|
+
const rows=await api.store.query(`SELECT b.session_id,b.descriptor_json FROM bindings b JOIN machines m ON m.team_id=b.team_id AND m.id=b.machine_id AND m.incarnation_id=b.incarnation_id AND m.revoked_at IS NULL AND m.last_seen_at>? WHERE b.team_id=?${sessionId?' AND b.session_id=?':''} ORDER BY b.id`,[api.clock.nowMs()-45000,teamId,...(sessionId?[sessionId]:[])]);
|
|
18
|
+
const bySession=new Map();
|
|
19
|
+
for(const row of rows){const reported=JSON.parse(row.descriptor_json),binding=Object.fromEntries(BINDING_FIELDS.filter(field=>field in reported).map(field=>[field,reported[field]]));const list=bySession.get(row.session_id)??[];list.push(binding);bySession.set(row.session_id,list);}
|
|
20
|
+
return bySession;
|
|
21
|
+
}
|
package/server/event-routes.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { appGrantAllows } from '@volter/teams/server/auth';
|
|
|
2
2
|
import { appAuthentication } from '@volter/teams/server/apps';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { validateListQuery } from '../contracts/index.mjs';
|
|
5
|
-
import { cursorHash, cursorToken } from './catalog.mjs';
|
|
5
|
+
import { cursorHash, cursorToken, liveBindings } from './catalog.mjs';
|
|
6
6
|
import { writeSse } from '@volter/teams/server/events';
|
|
7
7
|
|
|
8
8
|
function openStream(api, req, res, auth, requestId, fail) {
|
|
@@ -188,10 +188,13 @@ async function catalogWindow(api, auth, query) {
|
|
|
188
188
|
}
|
|
189
189
|
return readable(row);
|
|
190
190
|
};
|
|
191
|
-
|
|
191
|
+
// The window carries the bindings machines report now, so a stream (re-read on every change and every 15 s) tells a
|
|
192
|
+
// watcher when a session's doors open or close.
|
|
193
|
+
const items = [], bindings = await liveBindings(api, auth.team_id);
|
|
192
194
|
for (const row of rows) {
|
|
193
195
|
if (row.canonical_id || !readable(row)) continue;
|
|
194
196
|
const descriptor = JSON.parse(row.descriptor_json);
|
|
197
|
+
descriptor.bindings = bindings.get(row.id) ?? [];
|
|
195
198
|
for (const field of ['parent_id', 'continued_from']) if (descriptor[field] && !readableReference(descriptor[field])) descriptor[field] = null;
|
|
196
199
|
if (query.user && (query.user === 'me' ? auth.principal_id : query.user) !== descriptor.user?.principal_id) continue;
|
|
197
200
|
if (query.repo && query.repo !== descriptor.repository?.key) continue;
|
|
@@ -220,7 +223,8 @@ export async function handleCatalogEvents({ api, req, res, auth, url, requestId,
|
|
|
220
223
|
const enqueue = streamQueue(stream, async () => {
|
|
221
224
|
const page = await catalogWindow(api, auth, query);
|
|
222
225
|
// Authorization and expiry can change a window without creating a session revision.
|
|
223
|
-
|
|
226
|
+
// A binding's observed_at moves with every heartbeat; only a change of what the window says is a reset.
|
|
227
|
+
const digest = createHash('sha256').update(JSON.stringify(page.data, (key, value) => key === 'bindings' && Array.isArray(value) ? value.map(({ observed_at: _seen, ...binding }) => binding) : value)).digest('hex');
|
|
224
228
|
const cursor = `${page.revision}:${page.authEpoch}:${digest}`;
|
|
225
229
|
if (cursor !== lastCursor && stream.write({ type: 'catalog_reset', cursor, revision: page.revision, data: page.data })) lastCursor = cursor;
|
|
226
230
|
});
|
|
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto';
|
|
|
11
11
|
import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
|
|
12
12
|
import { storageReport } from './storage-report.mjs';
|
|
13
13
|
import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
|
|
14
|
-
import { normalizeSearch, cursorHash, cursorToken } from './catalog.mjs';
|
|
14
|
+
import { normalizeSearch, cursorHash, cursorToken, liveBindings } from './catalog.mjs';
|
|
15
15
|
import { handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
16
16
|
import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
|
|
17
17
|
import { exact } from '@volter/teams/server/request-json';
|
|
@@ -136,8 +136,9 @@ const managerGuard=(auth,sessionId,now,{governance=false}={})=>({
|
|
|
136
136
|
async function resolve(api,auth,ref){const head='SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND ',native=/^([a-z][a-z0-9-]*):(.+)$/.exec(ref);let rows;if(native)rows=await api.store.query(head+'s.id IN (SELECT session_id FROM origins WHERE team_id=? AND harness=? AND native_id=? AND deleted_at IS NULL)',[auth.team_id,auth.team_id,native[1],native[2]]);else if(ref.length>=36)rows=await api.store.query(head+'s.id=?',[auth.team_id,ref]);else if(ref.length>=8)rows=await api.store.query(head+'s.id LIKE ?',[auth.team_id,`${ref}%`]);else rows=[];const visible=[];for(let r of rows){r=await canonicalSession(api,auth.team_id,r);r=await api.store.one('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id=?',[auth.team_id,r.id]);if(r&&await canReadMerged(api,auth,r)&&!visible.some(x=>x.id===r.id))visible.push(r);}if(visible.length>1)fail(400,'ambiguous_reference','ambiguous session reference');if(!visible.length)fail(404,'not_found','not found');return visible[0];}
|
|
137
137
|
function qobj(url){return Object.fromEntries([...url.searchParams].map(([k,v])=>[k,['limit','since','until','tail','revision'].includes(k)?Number(v):k==='include_children'?v==='true':v]));}
|
|
138
138
|
function projection(row){return JSON.parse(row.descriptor_json);}
|
|
139
|
-
async function readerProjection(api,auth,row){
|
|
139
|
+
async function readerProjection(api,auth,row,bindings=null){
|
|
140
140
|
const view=projection(row);
|
|
141
|
+
view.bindings=(bindings??await liveBindings(api,auth.team_id,row.id)).get(row.id)??[];
|
|
141
142
|
for(const field of ['parent_id','continued_from']){
|
|
142
143
|
const reference=view[field];
|
|
143
144
|
if(!reference)continue;
|
|
@@ -177,11 +178,11 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
177
178
|
}else tuples=rows.filter(row=>row.revision===row.version_revision).map(row=>[row.id,row.version_revision]);
|
|
178
179
|
const rowByVersion=new Map(rows.map(row=>[`${row.id}:${row.version_revision}`,row])),grantsBySession=new Map();
|
|
179
180
|
for(const grant of allGrants){const list=grantsBySession.get(grant.session_id)??[];list.push(grant);grantsBySession.set(grant.session_id,list);}
|
|
180
|
-
const hits=[],hitTuples=[];
|
|
181
|
+
const hits=[],hitTuples=[],bindingsBySession=await liveBindings(api,auth.team_id);
|
|
181
182
|
for(const [id,revision]of tuples){
|
|
182
183
|
const row=rowByVersion.get(`${id}:${revision}`),current=currentById.get(id);
|
|
183
184
|
if(!row||!current||current.canonical_id||!await canReadMerged(api,{...auth,now:boundaryNow},current))continue;
|
|
184
|
-
const view=await readerProjection(api,auth,row);
|
|
185
|
+
const view=await readerProjection(api,auth,row,bindingsBySession);
|
|
185
186
|
if(query.user&&(query.user==='me'?auth.principal_id:query.user)!==view.user?.principal_id)continue;
|
|
186
187
|
if(query.repo&&query.repo!==view.repository?.key)continue;
|
|
187
188
|
if(query.harness&&query.harness!==view.harness)continue;
|
|
@@ -226,7 +227,7 @@ async function activityOf(api,teamId,sessionId){
|
|
|
226
227
|
async function sessionDescriptor(api,res,auth,id,requestId){
|
|
227
228
|
const row=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),bound:auth.bound});
|
|
228
229
|
const activity=await activityOf(api,auth.team_id,row.id);
|
|
229
|
-
send(res,200,result(api,auth,{...JSON.parse(row.descriptor_json),turnActivity:activity,working:activity.state==='working'?{since:activity.since}:null}),requestId,{ETag:`"${row.revision}"`});
|
|
230
|
+
send(res,200,result(api,auth,{...JSON.parse(row.descriptor_json),bindings:(await liveBindings(api,auth.team_id,row.id)).get(row.id)??[],turnActivity:activity,working:activity.state==='working'?{since:activity.since}:null}),requestId,{ETag:`"${row.revision}"`});
|
|
230
231
|
}
|
|
231
232
|
async function correlateRoute(api,req,res,auth,path,requestId){
|
|
232
233
|
if(!['user','service'].includes(auth.kind)||auth.role==='viewer')fail(403,'forbidden','credential cannot correlate sessions');
|
|
@@ -252,6 +253,7 @@ async function readSession(api,res,auth,id,url,requestId){
|
|
|
252
253
|
const {rows,beforeCursor,revision}=snapshot;
|
|
253
254
|
const messages=rows.map(row=>({id:row.id,source_id:row.source_id,native_key:row.native_key,recorded_at:row.recorded_at,value:JSON.parse(row.value_json)}));
|
|
254
255
|
const descriptor=JSON.parse(snapshot.descriptor_json);
|
|
256
|
+
descriptor.bindings=(await liveBindings(api,auth.team_id,descriptor.id)).get(descriptor.id)??[];
|
|
255
257
|
const activity=await activityOf(api,auth.team_id,descriptor.id);
|
|
256
258
|
send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},descriptor,turnActivity:activity,working:activity.state==='working'?{since:activity.since}:null,names:await sessionNames(api,auth.team_id,descriptor),messages,before_cursor:beforeCursor,event_cursor:snapshot.eventCursor,revision,display:{returned:messages.length,total:snapshot.total,truncated_fields:0}},requestId);
|
|
257
259
|
}
|