termdock 1.4.303 → 1.4.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/agent/integrationCli.js +11 -1
- package/dist/server/agent/integrationServer.js +2 -0
- package/dist/server/agent/integrationSessions.js +15 -3
- package/dist/server/agent/nativeResumeOwner.js +66 -12
- package/dist/server/routes/terminal.js +31 -23
- package/docs/integrations.md +63 -3
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -8,6 +8,7 @@ import { executeCollaborationCommand, duration } from './collaborationCli.js';
|
|
|
8
8
|
import { getTermdockVersion } from '../utils/version.js';
|
|
9
9
|
export const INTEGRATION_HELP = `td integration — local scoped integration administration
|
|
10
10
|
groups | list
|
|
11
|
+
diagnostics <TD-session-id> (read the last restore check; administrator only)
|
|
11
12
|
create --file <policy.json> --credential-file <private-output.json>
|
|
12
13
|
update --file <policy.json> (same identity/group; preserves the credential)
|
|
13
14
|
revoke <principal-id>
|
|
@@ -58,13 +59,22 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
58
59
|
write(INTEGRATION_HELP);
|
|
59
60
|
return 0;
|
|
60
61
|
}
|
|
62
|
+
const socketPath = integrationSocketPath(port);
|
|
63
|
+
if (action === 'diagnostics') {
|
|
64
|
+
if (argv.length !== 2 || !/^[a-zA-Z0-9._-]{1,80}$/.test(argv[1]))
|
|
65
|
+
throw new Error('diagnostics requires one TD session id');
|
|
66
|
+
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
67
|
+
if (capabilities.session_restore_diagnostics !== true)
|
|
68
|
+
throw new CollaborationError('SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED', 'Running service does not support stored restore diagnostics', 409);
|
|
69
|
+
write(JSON.stringify(parsed(await request(socketPath, 'GET', `/admin/sessions/${encodeURIComponent(argv[1])}/restore-diagnostics`, undefined, headers(undefined, adminToken)))));
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
61
72
|
for (let index = 1; index < argv.length; index += 2) {
|
|
62
73
|
const flag = argv[index];
|
|
63
74
|
if (!['--file', '--credential-file'].includes(flag) || !argv[index + 1] || options[flag])
|
|
64
75
|
throw new Error('Invalid integration arguments');
|
|
65
76
|
options[flag] = argv[index + 1];
|
|
66
77
|
}
|
|
67
|
-
const socketPath = integrationSocketPath(port);
|
|
68
78
|
if (action === 'groups' || action === 'list') {
|
|
69
79
|
if (argv.length !== 1)
|
|
70
80
|
throw new Error('Unexpected arguments');
|
|
@@ -61,6 +61,7 @@ export class IntegrationRuntime {
|
|
|
61
61
|
};
|
|
62
62
|
app.get('/admin/groups', run(req => { admin(req); return { groups: options.messages.list().filter(g => !g.federated).map(g => ({ id: g.id, name: g.name })) }; }));
|
|
63
63
|
app.get('/admin/capabilities', run(req => { admin(req); return this.capabilities(); }));
|
|
64
|
+
app.get('/admin/sessions/:id/restore-diagnostics', run(req => { admin(req); return this.sessions.restoreDiagnostics(req.params.id); }));
|
|
64
65
|
app.get('/admin/principals', run(req => { admin(req); return { integrations: this.store.policies() }; }));
|
|
65
66
|
app.post('/admin/principals', run(req => { admin(req); this.group(req.body?.groupId); const provisioned = this.store.provision(req.body); try {
|
|
66
67
|
this.sync();
|
|
@@ -145,6 +146,7 @@ export class IntegrationRuntime {
|
|
|
145
146
|
idempotent_session_create: true, exact_session_restore: true, external_validation: false,
|
|
146
147
|
startup_input_conditions: true,
|
|
147
148
|
integration_policy_update: true,
|
|
149
|
+
session_restore_diagnostics: true,
|
|
148
150
|
permissions: ['task.read', 'task.create', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore'] };
|
|
149
151
|
}
|
|
150
152
|
error(error) {
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
4
|
import { atomicJson, integrationError } from './integrationStore.js';
|
|
5
|
+
import { NativeResumeOwnerError } from './nativeResumeOwner.js';
|
|
5
6
|
/** Quote each configured argument as literal shell data, including paths. */
|
|
6
7
|
export function integrationLaunchCommand(record, restore) {
|
|
7
8
|
const launchArgs = record.profile.argv.map(arg => arg === '{cwd}' ? record.cwd : arg);
|
|
@@ -15,8 +16,8 @@ function startupInput(record, now) {
|
|
|
15
16
|
return condition ? { state: 'pending', deadline: now + (condition.timeoutMs ?? 120000), matched_since: null, observed_at: null } : undefined;
|
|
16
17
|
}
|
|
17
18
|
export function publicIntegrationSession(record) {
|
|
18
|
-
const { profile: _, deadline: _deadline, launch_submitted: _submitted, startup_condition: _condition, ...publicRecord } = record;
|
|
19
|
-
return publicRecord;
|
|
19
|
+
const { profile: _, deadline: _deadline, launch_submitted: _submitted, startup_condition: _condition, restore_diagnostics: diagnostics, ...publicRecord } = record;
|
|
20
|
+
return { ...publicRecord, ...(diagnostics ? { restore_diagnostics_available: true } : {}) };
|
|
20
21
|
}
|
|
21
22
|
export function permittedCwd(profile, cwd) {
|
|
22
23
|
if (typeof cwd !== 'string' || !path.isAbsolute(cwd) || /[\x00-\x1f\x7f]/.test(cwd))
|
|
@@ -168,7 +169,9 @@ export class IntegrationSessions {
|
|
|
168
169
|
catch (error) {
|
|
169
170
|
const current = this.doc.sessions.find(s => s.session_id === record.session_id);
|
|
170
171
|
const code = error.code;
|
|
171
|
-
this.saveRecord({ ...current, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_LAUNCH_FAILED',
|
|
172
|
+
this.saveRecord({ ...current, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_LAUNCH_FAILED',
|
|
173
|
+
restore_diagnostics: error instanceof NativeResumeOwnerError ? { ...structuredClone(error.diagnostics), operation_id: current.operation_id } : undefined,
|
|
174
|
+
updated_at: Date.now() });
|
|
172
175
|
}
|
|
173
176
|
return structuredClone(this.doc.sessions.find(s => s.session_id === record.session_id));
|
|
174
177
|
})().finally(() => this.pending.delete(record.session_id));
|
|
@@ -176,6 +179,14 @@ export class IntegrationSessions {
|
|
|
176
179
|
return promise;
|
|
177
180
|
}
|
|
178
181
|
async get(principal, id) { this.lookup(principal, id); await this.refresh(id); return this.lookup(principal, id); }
|
|
182
|
+
/** Read the stored check only: no process probe, PTY attachment, or new launch. */
|
|
183
|
+
restoreDiagnostics(id) {
|
|
184
|
+
const record = this.doc.sessions.find(session => session.session_id === id);
|
|
185
|
+
if (!record)
|
|
186
|
+
return integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Integration session does not exist', 404);
|
|
187
|
+
return structuredClone({ session_id: record.session_id, operation_id: record.operation_id,
|
|
188
|
+
state: record.state, error_code: record.error_code, diagnostics: record.restore_diagnostics ?? null });
|
|
189
|
+
}
|
|
179
190
|
async restore(principal, id, idempotencyKey) {
|
|
180
191
|
const request = this.request(principal, idempotencyKey, ['restore', id]);
|
|
181
192
|
if (request.previous)
|
|
@@ -215,6 +226,7 @@ export class IntegrationSessions {
|
|
|
215
226
|
record.operation_id = randomUUID();
|
|
216
227
|
record.deadline = Date.now() + this.bindingTimeout;
|
|
217
228
|
record.updated_at = Date.now();
|
|
229
|
+
record.restore_diagnostics = undefined;
|
|
218
230
|
if (!record.profile.startupInput && !record.startup_condition)
|
|
219
231
|
record.startup_condition = structuredClone(principal.launchProfiles.find(profile => profile.id === record.launch_profile)?.startupInput);
|
|
220
232
|
record.startup_input = startupInput(record, Date.now());
|
|
@@ -1,29 +1,83 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
1
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
|
+
/** A tmux remains observable without a browser/PTY attachment, including after
|
|
3
|
+
* service restart. Inventory UUIDs never filter the candidate set. */
|
|
4
|
+
export function collectNativeResumeOwnerCandidates(inventory, backends) {
|
|
5
|
+
const candidates = [];
|
|
6
|
+
const seenTmux = new Set(), seenBackend = new Set();
|
|
7
|
+
for (const entry of inventory) {
|
|
8
|
+
if (entry.mode !== 'tmux' || !entry.tmuxSessionName || seenTmux.has(entry.tmuxSessionName))
|
|
9
|
+
continue;
|
|
10
|
+
const backend = entry.backendSessionId ? backends.get(entry.backendSessionId) : null;
|
|
11
|
+
candidates.push({ backendSessionId: entry.backendSessionId ?? `inventory:${entry.sessionId}`,
|
|
12
|
+
sessionId: entry.sessionId, backendAttached: !!backend,
|
|
13
|
+
tmuxSessionName: entry.tmuxSessionName, cachedSlug: backend?.agent?.slug ?? entry.agentResume?.slug ?? null,
|
|
14
|
+
cachedNativeId: backend?.agentSession?.sessionId ?? entry.agentResume?.sessionId ?? null });
|
|
15
|
+
seenTmux.add(entry.tmuxSessionName);
|
|
16
|
+
if (entry.backendSessionId)
|
|
17
|
+
seenBackend.add(entry.backendSessionId);
|
|
18
|
+
}
|
|
19
|
+
for (const [backendSessionId, backend] of backends) {
|
|
20
|
+
if (seenBackend.has(backendSessionId) || backend.mode === 'tmux' && backend.tmuxSessionName && seenTmux.has(backend.tmuxSessionName))
|
|
21
|
+
continue;
|
|
22
|
+
candidates.push({ backendSessionId, sessionId: inventory.find(entry => entry.backendSessionId === backendSessionId)?.sessionId ?? null,
|
|
23
|
+
backendAttached: true, tmuxSessionName: backend.mode === 'tmux' ? backend.tmuxSessionName : null,
|
|
24
|
+
cachedSlug: backend.agent?.slug ?? null, cachedNativeId: backend.agentSession?.sessionId ?? null });
|
|
25
|
+
if (backend.mode === 'tmux' && backend.tmuxSessionName)
|
|
26
|
+
seenTmux.add(backend.tmuxSessionName);
|
|
27
|
+
}
|
|
28
|
+
return candidates;
|
|
29
|
+
}
|
|
30
|
+
export class NativeResumeOwnerError extends CollaborationError {
|
|
31
|
+
diagnostics;
|
|
32
|
+
constructor(code, blockers) {
|
|
33
|
+
super(code, code === 'NATIVE_SESSION_ALREADY_RUNNING'
|
|
34
|
+
? 'Another terminal process proves the exact native conversation is running'
|
|
35
|
+
: 'A possible native owner cannot be verified; inspect its terminal before restoring', 409);
|
|
36
|
+
// Preserve the proven owner even when earlier uncertain panes exceed the bound.
|
|
37
|
+
const ordered = code === 'NATIVE_SESSION_ALREADY_RUNNING' ? [blockers.at(-1), ...blockers.slice(0, -1)] : blockers;
|
|
38
|
+
this.diagnostics = { code, checked_at: Date.now(), candidates: ordered.slice(0, 128), total_blockers: blockers.length, truncated: blockers.length > 128 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function diagnostic(candidate, process, reason) {
|
|
42
|
+
// Store only named locator/evidence fields. Never serialize a callback error,
|
|
43
|
+
// argv, terminal text, cwd, or plugin/wrapper payload into diagnostics.
|
|
44
|
+
const text = (value) => typeof value === 'string' && value.length <= 256 && !/[\x00-\x1f\x7f]/.test(value) ? value : null;
|
|
45
|
+
const pid = (value) => Number.isInteger(value) && value > 0 ? value : null;
|
|
46
|
+
return { session_id: text(candidate.sessionId), backend_session_id: candidate.backendSessionId.startsWith('inventory:') ? null : text(candidate.backendSessionId),
|
|
47
|
+
backend_attached: candidate.backendAttached ?? null, tmux_session_name: text(candidate.tmuxSessionName),
|
|
48
|
+
tmux_session_id: text(process?.tmuxSessionId), pane_id: text(process?.paneId), pane_pid: pid(process?.panePid),
|
|
49
|
+
program: text(process?.program), process_source: text(process?.processSource), arguments_observed: process?.argumentsObserved === true,
|
|
50
|
+
agent_slug: text(process?.agentSlug), last_known_native_id: text(candidate.cachedNativeId), observed_native_id: text(process?.nativeId),
|
|
51
|
+
pgid: Number.isInteger(process?.pgid) ? process.pgid : null,
|
|
52
|
+
tpgid: Number.isInteger(process?.tpgid) ? process.tpgid : null, reason, observed_at: Date.now() };
|
|
53
|
+
}
|
|
54
|
+
/** Only process observations prove presence or absence of another owner.
|
|
55
|
+
* Cached UUIDs cannot exclude a terminal; the caller verifies its own pane. */
|
|
4
56
|
export async function assertNativeResumeAvailable(target, excludedBackendSessionId, candidates, observe) {
|
|
5
|
-
|
|
57
|
+
const blockers = [];
|
|
6
58
|
for (const candidate of candidates) {
|
|
7
59
|
if (candidate.backendSessionId === excludedBackendSessionId)
|
|
8
60
|
continue;
|
|
9
|
-
const cachedMatch = candidate.cachedSlug === target.slug && candidate.cachedNativeId === target.nativeSessionId;
|
|
10
61
|
let processes;
|
|
11
62
|
try {
|
|
12
63
|
processes = await observe(candidate);
|
|
13
64
|
}
|
|
14
65
|
catch {
|
|
15
|
-
|
|
16
|
-
uncertain = true;
|
|
66
|
+
blockers.push(diagnostic(candidate, null, 'TERMINAL_OBSERVATION_FAILED'));
|
|
17
67
|
continue;
|
|
18
68
|
}
|
|
19
69
|
for (const process of processes) {
|
|
20
70
|
if (process.confirmed && process.agentSlug === target.slug && process.nativeId === target.nativeSessionId) {
|
|
21
|
-
|
|
71
|
+
blockers.push(diagnostic(candidate, process, 'NATIVE_SESSION_MATCH'));
|
|
72
|
+
throw new NativeResumeOwnerError('NATIVE_SESSION_ALREADY_RUNNING', blockers);
|
|
22
73
|
}
|
|
23
|
-
if (
|
|
24
|
-
|
|
74
|
+
if (!process.confirmed)
|
|
75
|
+
blockers.push(diagnostic(candidate, process, process.shellFailure
|
|
76
|
+
?? (process.argumentsObserved === false ? 'PROCESS_ARGUMENTS_UNAVAILABLE' : 'FOREGROUND_PROCESS_UNCONFIRMED')));
|
|
77
|
+
else if (process.agentSlug === target.slug && !process.nativeId)
|
|
78
|
+
blockers.push(diagnostic(candidate, process, 'NATIVE_SESSION_ID_MISSING'));
|
|
25
79
|
}
|
|
26
80
|
}
|
|
27
|
-
if (
|
|
28
|
-
|
|
81
|
+
if (blockers.length)
|
|
82
|
+
throw new NativeResumeOwnerError('NATIVE_SESSION_OWNER_UNCONFIRMED', blockers);
|
|
29
83
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { collaborationSessionDisplayName } from '../utils/sessionDisplayName.js';
|
|
2
2
|
import { integrationError } from '../agent/integrationStore.js';
|
|
3
|
-
import { assertNativeResumeAvailable } from '../agent/nativeResumeOwner.js';
|
|
3
|
+
import { assertNativeResumeAvailable, collectNativeResumeOwnerCandidates } from '../agent/nativeResumeOwner.js';
|
|
4
4
|
import { integrationLaunchCommand } from '../agent/integrationSessions.js';
|
|
5
5
|
import { ensureTeam } from '../agent/collaborationTeam.js';
|
|
6
6
|
import { androidRecordings } from '../android/recording.js';
|
|
@@ -1825,19 +1825,26 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
1825
1825
|
/** Restricted session adapter: only preconfigured argv and the exact owned
|
|
1826
1826
|
* terminal can be launched; no remote drive or arbitrary shell commands. */
|
|
1827
1827
|
async function integrationPaneIsIdleShell(pane) {
|
|
1828
|
+
return (await inspectIntegrationShell(pane)).confirmed;
|
|
1829
|
+
}
|
|
1830
|
+
async function inspectIntegrationShell(pane) {
|
|
1828
1831
|
if (!pane.isShell)
|
|
1829
|
-
return false;
|
|
1832
|
+
return { confirmed: false, failure: null, pgid: null, tpgid: null };
|
|
1830
1833
|
try {
|
|
1831
1834
|
const { stdout } = await execFileAsync('ps', ['-p', String(pane.panePid), '-o', 'pgid=,tpgid=,args='], { timeout: 3000, maxBuffer: 16384 });
|
|
1832
|
-
const row = stdout.trim().match(/^(
|
|
1833
|
-
if (!row
|
|
1834
|
-
return false;
|
|
1835
|
+
const row = stdout.trim().match(/^(-?\d+)\s+(-?\d+)\s+(.+)$/);
|
|
1836
|
+
if (!row)
|
|
1837
|
+
return { confirmed: false, failure: 'SHELL_PROCESS_NOT_FOUND', pgid: null, tpgid: null };
|
|
1838
|
+
const pgid = Number(row[1]), tpgid = Number(row[2]);
|
|
1839
|
+
if (pgid <= 0 || pgid !== tpgid)
|
|
1840
|
+
return { confirmed: false, failure: 'SHELL_FOREGROUND_MISMATCH', pgid, tpgid };
|
|
1835
1841
|
const argv = splitCommandToArgv(row[3]);
|
|
1836
1842
|
const command = normalizeProgramName(argv[0]?.replace(/^-/, ''))?.toLowerCase();
|
|
1837
|
-
|
|
1843
|
+
const confirmed = !!command && shellNamesBackend.has(command) && argv.slice(1).every(arg => ['-l', '-i', '-il', '-li', '--login', '--interactive'].includes(arg));
|
|
1844
|
+
return { confirmed, failure: confirmed ? null : 'SHELL_ARGUMENTS_UNSUPPORTED', pgid, tpgid };
|
|
1838
1845
|
}
|
|
1839
1846
|
catch {
|
|
1840
|
-
return false;
|
|
1847
|
+
return { confirmed: false, failure: 'SHELL_OBSERVATION_FAILED', pgid: null, tpgid: null };
|
|
1841
1848
|
}
|
|
1842
1849
|
}
|
|
1843
1850
|
function integrationTerminalRecord(record) {
|
|
@@ -1910,43 +1917,44 @@ async function assertIntegrationNativeResumeAvailable(record) {
|
|
|
1910
1917
|
// Never use a recovered hook or the last-known UUID as live-owner proof.
|
|
1911
1918
|
if (process.platform === 'linux')
|
|
1912
1919
|
await getProcessSnapshot(true);
|
|
1913
|
-
const candidates =
|
|
1914
|
-
const persisted = globalSessionState.sessions.find(entry => entry.backendSessionId === backendSessionId)?.agentResume;
|
|
1915
|
-
return { backendSessionId, cachedSlug: backend.agent?.slug ?? persisted?.slug ?? null,
|
|
1916
|
-
cachedNativeId: backend.agentSession?.sessionId ?? persisted?.sessionId ?? null };
|
|
1917
|
-
});
|
|
1920
|
+
const candidates = collectNativeResumeOwnerCandidates(globalSessionState.sessions, terminalSessions);
|
|
1918
1921
|
await assertNativeResumeAvailable({ slug: record.agent_slug, nativeSessionId: record.agent_native_session_id }, own?.backendSessionId ?? null, candidates, async (candidate) => {
|
|
1919
1922
|
const backend = terminalSessions.get(candidate.backendSessionId);
|
|
1920
|
-
|
|
1921
|
-
return [];
|
|
1923
|
+
const tmuxSessionName = candidate.tmuxSessionName;
|
|
1922
1924
|
// Duplicate inventory attachments to the original owned pane are not
|
|
1923
1925
|
// another process; launchIntegrationTerminal verifies that pane again.
|
|
1924
|
-
if (
|
|
1926
|
+
if (tmuxSessionName && tmuxSessionName === own?.tmuxSessionName)
|
|
1925
1927
|
return [];
|
|
1926
|
-
if (
|
|
1928
|
+
if (tmuxSessionName) {
|
|
1927
1929
|
try {
|
|
1928
|
-
await runTmux(['has-session', '-t', `=${
|
|
1930
|
+
await runTmux(['has-session', '-t', `=${tmuxSessionName}`]);
|
|
1929
1931
|
}
|
|
1930
1932
|
catch (error) {
|
|
1931
1933
|
if (/can't find session|no server running|no sessions|error connecting.*No such file/i.test(getErrorMessage(error)))
|
|
1932
1934
|
return [];
|
|
1933
1935
|
throw error;
|
|
1934
1936
|
}
|
|
1935
|
-
const layout = await getTmuxLayout(
|
|
1937
|
+
const layout = await getTmuxLayout(tmuxSessionName);
|
|
1936
1938
|
return Promise.all(layout.windows.flatMap(window => window.panes).map(async (pane) => {
|
|
1937
1939
|
const program = await resolveTmuxPaneProgram(pane, true, true);
|
|
1938
1940
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1939
|
-
const shell =
|
|
1940
|
-
isShell: shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
1941
|
-
return { confirmed: shell || program?.source === 'tmux-tty' && Boolean(program.rawArgs),
|
|
1941
|
+
const shell = await inspectIntegrationShell({ panePid: pane.pid,
|
|
1942
|
+
isShell: !agent && shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
1943
|
+
return { confirmed: shell.confirmed || program?.source === 'tmux-tty' && Boolean(program.rawArgs),
|
|
1942
1944
|
agentSlug: agent?.slug ?? null,
|
|
1943
|
-
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null
|
|
1945
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null,
|
|
1946
|
+
paneId: pane.id, panePid: pane.pid, tmuxSessionId: layout.sessionId, program: program?.command ?? null,
|
|
1947
|
+
processSource: program?.source, argumentsObserved: !!program?.rawArgs,
|
|
1948
|
+
shellFailure: shell.failure, pgid: shell.pgid, tpgid: shell.tpgid };
|
|
1944
1949
|
}));
|
|
1945
1950
|
}
|
|
1951
|
+
if (!backend)
|
|
1952
|
+
return [];
|
|
1946
1953
|
const program = await detectShellActiveProgram(backend);
|
|
1947
1954
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1948
1955
|
return [{ confirmed: !!program && program.source !== 'unknown', agentSlug: agent?.slug ?? null,
|
|
1949
|
-
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null
|
|
1956
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null,
|
|
1957
|
+
program: program?.command ?? null, processSource: program?.source, argumentsObserved: !!program?.rawArgs }];
|
|
1950
1958
|
});
|
|
1951
1959
|
}
|
|
1952
1960
|
async function launchIntegrationTerminal(record, restoring, prepared) {
|
package/docs/integrations.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
启动输入条件与保留凭据的策略更新要求 **CLI / 实际运行服务均至少 1.4.301**;协议仍为 1。
|
|
5
5
|
插件原生 ID 的前台 argv 回读及服务重启后的运行中 restore 核验修复要求
|
|
6
6
|
**实际运行服务至少 1.4.302**。退出后的重复原生会话检查修复要求
|
|
7
|
-
**实际运行服务至少 1.4.303
|
|
7
|
+
**实际运行服务至少 1.4.303**。没有 PTY 附着的已登记 tmux 的重复保护修复要求
|
|
8
|
+
**实际运行服务至少 1.4.304**。管理员恢复诊断要求 **CLI / 实际服务均至少 1.4.305**;
|
|
9
|
+
建议 CLI 与服务同步安装 1.4.305(既有接入命令的 CLI 最低仍为 1.4.301)。
|
|
8
10
|
现有 `--session`、消息投递、任务报告与自动协作入口继续兼容;旧的
|
|
9
11
|
`task create --integration` 仍表示代码集成子任务,新身份使用 `--principal`。
|
|
10
12
|
|
|
@@ -303,9 +305,61 @@ operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒
|
|
|
303
305
|
返回 `NATIVE_SESSION_OWNER_UNCONFIRMED` 并停止恢复;应检查该终端,不绕过保护。
|
|
304
306
|
此检查覆盖服务已登记终端,不声称枚举其他应用中全部原生会话。
|
|
305
307
|
|
|
308
|
+
1.4.304 起,检查合并持久终端清单和当前 PTY backend;服务重启后无需网页挂接,
|
|
309
|
+
已登记 tmux 的每个 pane 都会现场核验。旧登记 UUID C 不会排除当前实际运行 UUID A
|
|
310
|
+
的终端,终端的协作路由为 recovering/offline 或绑定不匹配也不会跳过检查。
|
|
311
|
+
仅在确认 tmux 已消失或当前进程排除冲突时继续;未知进程、同 Agent 缺少精确 UUID、
|
|
312
|
+
观察失败均返回 `NATIVE_SESSION_OWNER_UNCONFIRMED`,不以旧 UUID 证明没有冲突。
|
|
313
|
+
|
|
314
|
+
### 管理员恢复诊断(1.4.305)
|
|
315
|
+
|
|
316
|
+
`capabilities.session_restore_diagnostics=true` 表示服务支持持久恢复诊断。
|
|
317
|
+
遇到 `NATIVE_SESSION_OWNER_UNCONFIRMED` 或 `NATIVE_SESSION_ALREADY_RUNNING` 后,
|
|
318
|
+
本机管理员执行:
|
|
319
|
+
|
|
320
|
+
```sh
|
|
321
|
+
td integration diagnostics <TD-session-id>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
命令经原私有 Unix socket 和本机管理员凭据读取报告,不使用 `--principal`。
|
|
325
|
+
服务能力缺失时返回 `SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED`,不会假装诊断成功。
|
|
326
|
+
命令仅读取已存记录,不重新扫描进程、不挂接 PTY、不试键、不创建恢复意图。
|
|
327
|
+
304 及更早的失败没有此报告;升级后应使用新的 restore 幂等键执行一次明确检查,
|
|
328
|
+
再读报告。旧键不会执行一次新检查。
|
|
329
|
+
|
|
330
|
+
响应包含当前 `session_id / operation_id / state / error_code`,以及可为空的
|
|
331
|
+
`diagnostics`。报告含其自身 `operation_id / checked_at / code`、`candidates`、
|
|
332
|
+
`total_blockers / truncated`;它是该次检查的历史事实,不能代表读取时的当前进程。
|
|
333
|
+
只有上一次**实际发起**的恢复检查会保存报告;下一次实际恢复清除旧报告,运行中
|
|
334
|
+
no-op restore 不创建新检查。报告跟随会话保存并可在服务重启后回读。
|
|
335
|
+
|
|
336
|
+
每个阻塞候选含 `session_id / backend_session_id / backend_attached`、
|
|
337
|
+
`tmux_session_name / tmux_session_id / pane_id / pane_pid`、`program / process_source`、
|
|
338
|
+
`arguments_observed / agent_slug / last_known_native_id / observed_native_id`、
|
|
339
|
+
`pgid / tpgid / reason / observed_at`;无法观测的值为 null。backend 不存在或仅有旧
|
|
340
|
+
登记仍可通过 tmux 核验;`pane_pid` 是承载 pane 的 PID,不冒充 Agent 前台 PID。
|
|
341
|
+
报告最多保存 128 个阻塞候选;`total_blockers` 保留总数,确定的同 UUID 所有者优先展示。
|
|
342
|
+
|
|
343
|
+
| reason | 实际证据缺口或冲突 |
|
|
344
|
+
| --- | --- |
|
|
345
|
+
| NATIVE_SESSION_MATCH | 当前前台进程明确携带目标 UUID |
|
|
346
|
+
| NATIVE_SESSION_ID_MISSING | 当前同 Agent 的进程已观测,但没有可回读的精确 UUID |
|
|
347
|
+
| PROCESS_ARGUMENTS_UNAVAILABLE | 无法取得可靠的当前进程 argv |
|
|
348
|
+
| FOREGROUND_PROCESS_UNCONFIRMED | 无法可靠核验前台进程 |
|
|
349
|
+
| SHELL_PROCESS_NOT_FOUND | shell 的 ps 输出未形成可解析记录 |
|
|
350
|
+
| SHELL_FOREGROUND_MISMATCH | shell 未持有有效前台进程组;记录 pgid/tpgid |
|
|
351
|
+
| SHELL_ARGUMENTS_UNSUPPORTED | 当前 shell 命令或启动参数未满足既有安全检查 |
|
|
352
|
+
| SHELL_OBSERVATION_FAILED | shell 的进程观察失败 |
|
|
353
|
+
| TERMINAL_OBSERVATION_FAILED | tmux 或终端观察失败,无法排除该候选 |
|
|
354
|
+
|
|
355
|
+
完整候选可能来自其他组,因此仅管理员接口返回;principal 的 session 响应与推送
|
|
356
|
+
事件只增加 `restore_diagnostics_available=true`,不含候选标识/UUID。报告不保存或
|
|
357
|
+
返回原始 argv、cwd、终端正文、包装器异常、凭据。诊断不自动放宽任何拒绝条件,
|
|
358
|
+
不能把旧 UUID、native ready 或 Agent 忙闲推测当作消除冲突的证据。
|
|
359
|
+
|
|
306
360
|
常见失败码(每项都是完整字符串):
|
|
307
361
|
|
|
308
|
-
- 配置/权限:`LAUNCH_PROFILE_DENIED`, `LAUNCHER_UNAVAILABLE`, `COLLAB_AGENT_UNAVAILABLE`, `SESSION_CWD_MISSING`, `SESSION_CWD_DENIED`, `INTEGRATION_SESSION_NOT_FOUND`, `INTEGRATION_SESSION_LIMIT`。
|
|
362
|
+
- 配置/权限:`LAUNCH_PROFILE_DENIED`, `LAUNCHER_UNAVAILABLE`, `COLLAB_AGENT_UNAVAILABLE`, `SESSION_CWD_MISSING`, `SESSION_CWD_DENIED`, `INTEGRATION_SESSION_NOT_FOUND`, `INTEGRATION_SESSION_LIMIT`, `SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED`。
|
|
309
363
|
- 终端/身份:`SESSION_BACKEND_UNAVAILABLE`, `SESSION_TARGET_NOT_SHELL`, `SESSION_IDENTITY_MISMATCH`, `NATIVE_SESSION_ID_MISMATCH`, `NATIVE_SESSION_ID_INVALID`, `NATIVE_SESSION_ID_MISSING`。
|
|
310
364
|
- 启动/恢复:`SESSION_CREATE_OUTCOME_UNKNOWN`, `SESSION_OPERATION_IN_PROGRESS`, `SESSION_BINDING_TIMEOUT`, `EXACT_RESUME_UNSUPPORTED`, `NATIVE_SESSION_ALREADY_RUNNING`, `NATIVE_SESSION_OWNER_UNCONFIRMED`, `AGENT_NOT_RUNNING`。
|
|
311
365
|
- 未分类异常:`SESSION_LAUNCH_FAILED`, `SESSION_OBSERVATION_FAILED`;错误不包含包装器输出、密钥或原始堆栈。
|
|
@@ -325,7 +379,13 @@ operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒
|
|
|
325
379
|
不重复启动、不同 UUID 拒绝,以及进程退出后缓存 UUID 不再阻挡恢复。使用方已报告
|
|
326
380
|
1.4.302 真实 TraeX 服务重启回读、运行中恢复不重启、不同 UUID 拒绝通过;显式恢复
|
|
327
381
|
原 UUID 也已回读 ready,但配置的模型 footer 条件被暂态提示替换,输入仍保持待投递。
|
|
328
|
-
|
|
382
|
+
使用方已报告 1.4.303 退出后精确恢复通过,也发现未挂接 PTY 的已登记 tmux 重复保护
|
|
383
|
+
遗漏;维护机已用“旧 UUID C、实际运行 UUID A、无 PTY 附着”的真实隔离 tmux 复现,
|
|
384
|
+
并验证 1.4.304 编译后检查在该状态下拒绝再次启动。
|
|
385
|
+
使用方已报告 1.4.304 在未挂接网页/PTY、旧登记 UUID C 而实际运行 A 的场景下,
|
|
386
|
+
正确拒绝同 UUID 恢复且没有第二个进程。所有者退出后仍有未确认候选,不能仅凭
|
|
387
|
+
错误码判断是哪一个 pane;1.4.305 提供管理员持久诊断,并保持原拒绝条件。
|
|
388
|
+
**1.4.305 的真实 TraeX 阻塞候选诊断与稳定输入条件仍须使用方实机复验**。
|
|
329
389
|
回读 capabilities 后,应核对原 TD ID、真实 TraeX UUID 与原 argv/cwd/包装器快照,
|
|
330
390
|
并验证 Agent 真正加载原对话。
|
|
331
391
|
先完成测试环境的创建→追问→关联答复,再验重复键、追加咨询、断线重投和精确恢复。
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.305",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-E9GDa4nrCXADRXdyPKufusZt6IcCr4m2mGdx5h34KMQ=",
|
|
9
|
-
"serverBundleHash": "sha256
|
|
9
|
+
"serverBundleHash": "sha256-XkM6+bMn6xdVpuXe8oaPunofP3J+MgVfh7lwiOhSY4I=",
|
|
10
10
|
"clientBundleHash": "sha256-HOoNMOYhnph9XwUhSYE5W2CdI9Vb4g0c64ubW49LnXE=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|