@volter/supercode-teams 0.2.36 → 0.2.38
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/connector/index.mjs +17 -3
- package/machine.mjs +59 -1
- package/package.json +1 -1
package/connector/index.mjs
CHANGED
|
@@ -60,6 +60,12 @@ const CHUNK_BYTES=1024*1024;
|
|
|
60
60
|
// A minute is four beats with no write: the turn is over, or waiting on the user or one long tool call.
|
|
61
61
|
// Smaller files translate in well under a second and keep getting a bundle with every capture.
|
|
62
62
|
const NATIVE_IDLE_MS=60000,NATIVE_IDLE_BYTES=8*1024*1024;
|
|
63
|
+
// Loading a transcript reads all of it, and a live one changes on every message: a 135 MB Claude transcript took
|
|
64
|
+
// over 30 s a load on a Windows PC, on every beat, and translating it held the harness server past 272 s while every
|
|
65
|
+
// other request queued behind. A file of LARGE_CAPTURE_BYTES or more is captured at most once per
|
|
66
|
+
// LARGE_CAPTURE_EVERY_MS (tried or not), its load given a second per megabyte, and one past NATIVE_MAX_BYTES is
|
|
67
|
+
// published without a native bundle, which could not be built in time.
|
|
68
|
+
const LARGE_CAPTURE_BYTES=8*1024*1024,LARGE_CAPTURE_EVERY_MS=5*60000,NATIVE_MAX_BYTES=64*1024*1024;
|
|
63
69
|
const fileStamp=stat=>`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
64
70
|
const recordedAt=entry=>{if(Number.isSafeInteger(entry.recorded_at))return entry.recorded_at;if(Number.isSafeInteger(entry.timestamp))return entry.timestamp;const value=entry.metadata?.timestamp??entry.value?.metadata?.timestamp??entry.timestamp;if(typeof value!=='string')return null;const parsed=Date.parse(value);return Number.isSafeInteger(parsed)?parsed:null;};
|
|
65
71
|
const serverOrigin=value=>{const url=new URL(value),loopback=url.hostname==='localhost'||url.hostname==='::1'||url.hostname==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(url.hostname);if(!['http:','https:'].includes(url.protocol)||url.username||url.password||url.search||url.hash||(url.pathname!==''&&url.pathname!=='/')||(url.protocol==='http:'&&!loopback))throw new TypeError('server must be an HTTPS origin (HTTP is loopback-only)');return url.origin;};
|
|
@@ -151,7 +157,13 @@ export class TeamsConnector {
|
|
|
151
157
|
if (running || this.abort.signal.aborted) return null;
|
|
152
158
|
running = true;
|
|
153
159
|
const run = (async () => { this.localRoots = await roots(); await pass(); })();
|
|
154
|
-
|
|
160
|
+
// A pass that fails is said in the connector's log (once per distinct failure): nothing syncs until it passes.
|
|
161
|
+
void run.catch(error => {
|
|
162
|
+
this.lastError = error;
|
|
163
|
+
const said = `${error?.code ?? ''} ${error?.message ?? error}`;
|
|
164
|
+
if (said !== this.lastPassFailure) process.stderr.write(`supercode teams: a sync pass failed: ${error?.message ?? error}\n`);
|
|
165
|
+
this.lastPassFailure = said;
|
|
166
|
+
}).then(() => { if (run && !this.lastError) this.lastPassFailure = null; }).finally(() => { running = false; });
|
|
155
167
|
return run;
|
|
156
168
|
};
|
|
157
169
|
/** The first sync pass, for a caller that waits on it; the connector keeps running if it fails. */
|
|
@@ -392,11 +404,13 @@ export class TeamsConnector {
|
|
|
392
404
|
if(!configuration)configuration=await this.configuration();const rule=configuration.rules.find(item=>item.id===ruleId&&item.revision===ruleRevision&&item.state==='active');if(!rule)throw Object.assign(new Error('sync rule is absent, inactive, or stale'),{code:'forbidden'});visibility=rule.visibility;
|
|
393
405
|
const artifactSource=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
|
|
394
406
|
const artifactBefore=artifactSource?statSync(artifactSource):null;
|
|
395
|
-
const
|
|
407
|
+
const large=(artifactBefore?.size??0)>=LARGE_CAPTURE_BYTES,sessionKey=`${harness}:${nativeSessionId}`;
|
|
408
|
+
if(large){this.largeCaptures??=new Map();const last=this.largeCaptures.get(sessionKey);if(last!=null&&this.clock.nowMs()-last<LARGE_CAPTURE_EVERY_MS)return null;this.largeCaptures.set(sessionKey,this.clock.nowMs());}
|
|
409
|
+
const loaded=await this.core.session(locator).load(large?{timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)}:undefined),configuredBackfill=rule.backfill??backfill??'all',backfillPolicy=typeof configuredBackfill==='string'?{mode:configuredBackfill,since:null}:configuredBackfill;
|
|
396
410
|
const key=`${harness}:${nativeSessionId}`,previous=this.state.origins[key]??null;
|
|
397
411
|
let messages=(loaded.messages??loaded.turns??[]).map((entry,index)=>{const value=entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key)));return{native_key:entry.id??entry.native_key??String(index),recorded_at:recordedAt(entry),value};});
|
|
398
412
|
// A session published without its native bundle is captured again, with the same messages, once the bundle can be built.
|
|
399
|
-
const nativeWanted=backfillPolicy.mode==='all'&&typeof this.core.translateSession==='function';
|
|
413
|
+
const nativeWanted=backfillPolicy.mode==='all'&&typeof this.core.translateSession==='function'&&!((artifactBefore?.size??0)>NATIVE_MAX_BYTES);
|
|
400
414
|
const retryNative=Boolean(previous?.native_deferred&&nativeWanted&&artifactBefore?.isFile()&&!previous?.baseline&&this.nativeReady(key,artifactBefore));
|
|
401
415
|
const unchanged=previous?.last_rule_revision===ruleRevision&&previous?.backfill_mode===backfillPolicy.mode&&previous?.observed_keys?.length===messages.length&&messages.every((message,index)=>message.native_key===previous.observed_keys[index]&&hash(canonicalJson(message.value))===previous.observed_digests?.[index]);
|
|
402
416
|
if(unchanged&&!retryNative){this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
|
package/machine.mjs
CHANGED
|
@@ -68,6 +68,31 @@ const COMPOSER_RECHECK_MS = 400;
|
|
|
68
68
|
* must have stopped changing for this long before the pane counts as ready. */
|
|
69
69
|
const INITIAL_INPUT_SETTLE_MS = 1_000;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Claude asks whether to trust a folder it has not opened before, whatever its permission mode, and has no flag that
|
|
73
|
+
* skips the question; its default answer exits, taking the pane with it. A Claude this machine launches without
|
|
74
|
+
* prompts has its folder recorded as trusted in this machine's Claude config (`projects[<folder>]` in .claude.json,
|
|
75
|
+
* keyed as Claude keys it: the real path, with forward slashes on Windows), as a Codex launch has its trust passed on
|
|
76
|
+
* its command line. A whole new file is renamed over the old one, since Claude rewrites it while it runs.
|
|
77
|
+
*/
|
|
78
|
+
function trustClaudeFolder(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
const config = join(process.env.CLAUDE_CONFIG_DIR || homedir(), '.claude.json');
|
|
81
|
+
const real = realpathSync(cwd);
|
|
82
|
+
const folder = process.platform === 'win32' ? real.replace(/^\\\\\?\\/, '').replaceAll('\\', '/') : real;
|
|
83
|
+
let root = {};
|
|
84
|
+
try { root = JSON.parse(readFileSync(config, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') return; }
|
|
85
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return;
|
|
86
|
+
const projects = root.projects ??= {};
|
|
87
|
+
const project = projects[folder] ??= {};
|
|
88
|
+
if (project.hasTrustDialogAccepted === true) return;
|
|
89
|
+
project.hasTrustDialogAccepted = true;
|
|
90
|
+
const staged = `${config}.supercode-${process.pid}`;
|
|
91
|
+
writeFileSync(staged, JSON.stringify(root, null, 2));
|
|
92
|
+
renameSync(staged, config);
|
|
93
|
+
} catch { /* Claude asks as it always did */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
/** How long a launch watches its new pane before answering, so a program that ends at once is reported. */
|
|
72
97
|
const LAUNCH_SETTLE_MS = 500;
|
|
73
98
|
|
|
@@ -353,9 +378,10 @@ export class TeamsMachine extends MachineCore {
|
|
|
353
378
|
// receiver named rather than addressed needs `mail:*`.
|
|
354
379
|
const { admitted, principal } = entry;
|
|
355
380
|
const request = stream.params?.request;
|
|
381
|
+
if (request?.op === 'turn') return this.#serveTurn(entry, request, stream, accept, refuse);
|
|
356
382
|
const to = typeof request?.to === 'string' ? request.to : '*';
|
|
357
383
|
const target = to.startsWith('sc:') ? `mail:${to}` : 'mail:*';
|
|
358
|
-
if (!request || !['send', 'file', 'list'].includes(request.op)) return refuse('mail needs a send, file or
|
|
384
|
+
if (!request || !['send', 'file', 'list'].includes(request.op)) return refuse('mail needs a send, file, list or turn request');
|
|
359
385
|
if (!holds(admitted, 'mail', target)) {
|
|
360
386
|
this.log.append({ principal, cap: 'mail', target, outcome: 'refused' });
|
|
361
387
|
return refuse(`no mail grant for ${target}`);
|
|
@@ -377,6 +403,37 @@ export class TeamsMachine extends MachineCore {
|
|
|
377
403
|
child.stdin.end(JSON.stringify({ ...request, via_principal: principal ?? null }));
|
|
378
404
|
}
|
|
379
405
|
|
|
406
|
+
#serveTurn(entry, request, stream, accept, refuse) {
|
|
407
|
+
// A person's line typed into one session here as that session's own user turn (a Room member in a Conversation
|
|
408
|
+
// bound to the session): `interact` on `session:<harness>/<id>`. Who typed it comes from the caller's claim
|
|
409
|
+
// (the name its workplace signed), never from the request.
|
|
410
|
+
const { admitted, principal } = entry;
|
|
411
|
+
// A caller the server admitted for one session (a Room member's token) names nothing: its grant names the session.
|
|
412
|
+
const granted = admitted?.class === 'operator' ? [] : (admitted?.grants ?? []).filter((grant) => grant.caps.includes('interact')).flatMap((grant) => grant.targets).filter((target) => target.startsWith('session:') && target !== 'session:*');
|
|
413
|
+
const named = typeof request.harness === 'string' && typeof request.session === 'string' ? `${request.harness}/${request.session}` : (granted.length === 1 ? granted[0].slice('session:'.length) : '');
|
|
414
|
+
const [harness, session] = named.includes('/') ? [named.slice(0, named.indexOf('/')), named.slice(named.indexOf('/') + 1)] : ['', ''];
|
|
415
|
+
const target = `session:${harness}/${session}`;
|
|
416
|
+
if (!harness || !session || typeof request.text !== 'string' || !request.text.trim()) return refuse('a turn needs harness, session and text');
|
|
417
|
+
if (!holds(admitted, 'interact', target)) {
|
|
418
|
+
this.log.append({ principal, cap: 'interact', target, outcome: 'refused' });
|
|
419
|
+
return refuse(`no interact grant for ${target}`);
|
|
420
|
+
}
|
|
421
|
+
accept();
|
|
422
|
+
const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
|
|
423
|
+
let out = '';
|
|
424
|
+
let err = '';
|
|
425
|
+
child.stdout.on('data', (chunk) => { out += chunk; });
|
|
426
|
+
child.stderr.on('data', (chunk) => { err += chunk; });
|
|
427
|
+
child.on('error', (error) => { err += error.message; });
|
|
428
|
+
child.on('close', (code) => {
|
|
429
|
+
this.log.append({ principal, cap: 'interact', target, outcome: code === 0 ? 'turn' : 'failed', detail: err.trim().slice(0, 200) || undefined });
|
|
430
|
+
if (stream.closed) return;
|
|
431
|
+
stream.write(out.trim() || JSON.stringify({ code: 1, text: err.trim() || `remote-deliver exited ${code}` }));
|
|
432
|
+
stream.close('delivered');
|
|
433
|
+
});
|
|
434
|
+
child.stdin.end(JSON.stringify({ op: 'turn', harness, session, text: request.text, from_name: entry.name ?? principal ?? 'a Room member', via: 'room' }));
|
|
435
|
+
}
|
|
436
|
+
|
|
380
437
|
// ---- terminal ------------------------------------------------------------
|
|
381
438
|
|
|
382
439
|
async #attachTerminal(entry, stream, accept, refuse) {
|
|
@@ -989,6 +1046,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
989
1046
|
}
|
|
990
1047
|
const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
|
|
991
1048
|
this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, status: 'issued' });
|
|
1049
|
+
if (params.kind === 'claude-code' && params.cwd && (params.arguments ?? []).includes('--dangerously-skip-permissions')) trustClaudeFolder(params.cwd);
|
|
992
1050
|
let session;
|
|
993
1051
|
try {
|
|
994
1052
|
session = await this.host.createSession({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
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",
|