termdock 1.4.348 → 1.4.349
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/collaborationCli.js +2 -0
- package/dist/server/agent/integrationCli.js +20 -0
- package/dist/server/agent/integrationCloseOwnership.js +18 -0
- package/dist/server/agent/integrationServer.js +3 -0
- package/dist/server/agent/integrationSessions.js +14 -0
- package/dist/server/routes/terminal.js +58 -9
- package/docs/integrations.md +60 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -95,6 +95,8 @@ export const COLLAB_HELP = `td collab — durable messages; no agent-specific ho
|
|
|
95
95
|
generation递增;原launch operation_id保持。rename_pending=true时用get对账显示同步。
|
|
96
96
|
session close <id> --generation <N> --idempotency-key <key>
|
|
97
97
|
权限 session.close;显式终止精确自有Agent/原失败空shell,移除活动承载;保留历史与诊断。
|
|
98
|
+
1.4.349 缺活动终端登记时,Linux 仅凭唯一持久归属、原单pane及实时writer-lock身份核验关闭;
|
|
99
|
+
管理员用 td integration close-diagnostics <id> 只读核验,不按名称/argv/旧PID强制关闭。
|
|
98
100
|
closed终态拒绝send/reply/restore/retry-cold;release才是保留精确恢复的入口。
|
|
99
101
|
有pending/进行中操作/身份不明确时拒绝;中断只对账不自动再终止,close_pending=true表示结果未确认。
|
|
100
102
|
rename/close最低CLI与实际服务1.4.315;session_rename/session_close=true;两个权限默认不授权。
|
|
@@ -66,6 +66,17 @@ export const INTEGRATION_HELP = `td integration — local scoped integration adm
|
|
|
66
66
|
Minimum CLI/running service 1.4.309; capabilities.session_resume_configuration=true.
|
|
67
67
|
Errors: RESUME_CONFIGURATION_CHANGED, EXACT_RESUME_UNSUPPORTED,
|
|
68
68
|
SESSION_GENERATION_MISMATCH, SESSION_OPERATION_IN_PROGRESS, LAUNCHER_UNAVAILABLE.
|
|
69
|
+
close-diagnostics <TD-session-id> (fresh read-only close target; administrator only)
|
|
70
|
+
Minimum CLI/running service 1.4.349; session_close_diagnostics=true.
|
|
71
|
+
Returns {session_id,operation_id,generation,close_diagnostics:{checked_at,
|
|
72
|
+
terminal_registration_present,binding_present,runtime_present,pending_messages,
|
|
73
|
+
target_confirmed,code,target}}. target has exact pane/native/process instance facts,
|
|
74
|
+
never argv/environment/output. Does not change records, routing or generation,
|
|
75
|
+
close/restore a session, send keys or grant permission for a later close.
|
|
76
|
+
Linux session_close_detached_registration=true supports existing session.close
|
|
77
|
+
when active terminal inventory is missing: unique durable local ownership,
|
|
78
|
+
original single pane and live writer-lock native identity are still required.
|
|
79
|
+
No forced close by tmux name, argv UUID, stale PID or shell-only observation.
|
|
69
80
|
diagnostics <TD-session-id> (read stored launch/restore diagnostics; administrator only)
|
|
70
81
|
1.4.314 adds phase/timestamps, paste/submit write facts and target-change
|
|
71
82
|
evidence; no argv/environment/credentials or application readiness claim.
|
|
@@ -226,6 +237,15 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
226
237
|
write(JSON.stringify(parsed(await request(socketPath, body ? 'POST' : 'GET', `/admin/sessions/${encodeURIComponent(id)}/resume-configuration`, body, headers(undefined, adminToken)))));
|
|
227
238
|
return 0;
|
|
228
239
|
}
|
|
240
|
+
if (action === 'close-diagnostics') {
|
|
241
|
+
if (argv.length !== 2 || !/^[a-zA-Z0-9._-]{1,80}$/.test(argv[1]))
|
|
242
|
+
throw new Error('close-diagnostics requires one full TD session id');
|
|
243
|
+
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
244
|
+
if (capabilities.session_close_diagnostics !== true)
|
|
245
|
+
throw new CollaborationError('SESSION_CLOSE_DIAGNOSTICS_UNSUPPORTED', 'CLI and running service must support live close diagnostics (minimum 1.4.349)', 409);
|
|
246
|
+
write(JSON.stringify(parsed(await request(socketPath, 'GET', `/admin/sessions/${encodeURIComponent(argv[1])}/close-diagnostics`, undefined, headers(undefined, adminToken)))));
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
229
249
|
if (action === 'diagnostics') {
|
|
230
250
|
if (argv.length !== 2 || !/^[a-zA-Z0-9._-]{1,80}$/.test(argv[1]))
|
|
231
251
|
throw new Error('diagnostics requires one TD session id');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { integrationError } from './integrationStore.js';
|
|
2
|
+
/** Missing UI inventory is not authority to kill a named tmux session. Require
|
|
3
|
+
* independent durable ownership first; live pane/native/process proof follows.
|
|
4
|
+
* No terminal registration, route or group membership is created here. */
|
|
5
|
+
export function assertDetachedCloseOwnership(e) {
|
|
6
|
+
if (!e.localGroup || !e.sessionId || !e.groupId || !e.principalId
|
|
7
|
+
|| e.descriptorOwners.length !== 1 || e.descriptorOwners[0]?.sessionId !== e.sessionId
|
|
8
|
+
|| e.descriptorOwners[0]?.groupId !== e.groupId || e.descriptorOwners[0]?.principalId !== e.principalId
|
|
9
|
+
|| e.principalOwners.length !== 1 || e.principalOwners[0]?.principalId !== e.principalId
|
|
10
|
+
|| e.principalOwners[0]?.groupId !== e.groupId || e.principalOwners[0]?.revoked
|
|
11
|
+
|| e.groupOwners.length !== 1 || e.groupOwners[0] !== e.groupId) {
|
|
12
|
+
integrationError('SESSION_CLOSE_OWNERSHIP_UNCONFIRMED', 'Missing terminal registration requires unique durable local session, principal and group ownership', 409);
|
|
13
|
+
}
|
|
14
|
+
if (e.terminalClaims || e.backendClaims.length > 1 || e.otherPaneOwner
|
|
15
|
+
|| e.backendClaims.some(b => b.foreignTerminal || b.routeOwner !== null && b.routeOwner !== e.sessionId)) {
|
|
16
|
+
integrationError('SESSION_IDENTITY_MISMATCH', 'Another terminal, backend or route claims the close target', 409);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -131,6 +131,7 @@ export class IntegrationRuntime {
|
|
|
131
131
|
}));
|
|
132
132
|
app.get('/admin/capabilities', run(req => { admin(req); return this.capabilities(); }));
|
|
133
133
|
app.get('/admin/sessions/:id/restore-diagnostics', run(req => { admin(req); return { ...this.sessions.restoreDiagnostics(req.params.id), ...this.sessions.launchDiagnostics(req.params.id) }; }));
|
|
134
|
+
app.get('/admin/sessions/:id/close-diagnostics', run(async (req) => { admin(req); return this.sessions.closeDiagnostics(req.params.id); }));
|
|
134
135
|
app.get('/admin/sessions/:id/resume-configuration', run(async (req) => { admin(req); return this.sessions.resumeConfiguration(req.params.id); }));
|
|
135
136
|
app.post('/admin/sessions/:id/resume-configuration', run(async (req) => { admin(req); return { session: publicIntegrationSession(await this.sessions.confirmResumeConfiguration(req.params.id, req.body?.generation, req.body?.fingerprint)) }; }));
|
|
136
137
|
app.post('/admin/sessions/:id/startup-input/clear', run(async (req) => {
|
|
@@ -312,6 +313,8 @@ export class IntegrationRuntime {
|
|
|
312
313
|
session_cold_retry: true, session_cold_retry_min_version: '1.4.314',
|
|
313
314
|
session_rename: true, session_rename_min_version: '1.4.315',
|
|
314
315
|
session_close: true, session_close_min_version: '1.4.315',
|
|
316
|
+
session_close_diagnostics: true, session_close_diagnostics_min_version: '1.4.349',
|
|
317
|
+
session_close_detached_registration: process.platform === 'linux', session_close_detached_registration_min_version: '1.4.349',
|
|
315
318
|
admin_local_group_create: true, admin_local_group_create_min_version: '1.4.318',
|
|
316
319
|
admin_local_group_rename: true, admin_local_group_rename_min_version: '1.4.319',
|
|
317
320
|
admin_local_group_archive: true, admin_local_group_archive_min_version: '1.4.323',
|
|
@@ -263,6 +263,20 @@ export class IntegrationSessions {
|
|
|
263
263
|
return structuredClone({ launch_diagnostics: record.launch_diagnostics ?? null,
|
|
264
264
|
launch_diagnostics_history: record.launch_diagnostics_history ?? [] });
|
|
265
265
|
}
|
|
266
|
+
async closeDiagnostics(id) {
|
|
267
|
+
const record = structuredClone(this.doc.sessions.find(s => s.session_id === id));
|
|
268
|
+
if (!record)
|
|
269
|
+
return integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Integration session does not exist', 404);
|
|
270
|
+
if (!this.adapter.closeDiagnostics)
|
|
271
|
+
integrationError('SESSION_CLOSE_DIAGNOSTICS_UNSUPPORTED', 'Live close diagnostics are unavailable', 409);
|
|
272
|
+
const report = await this.adapter.closeDiagnostics(record);
|
|
273
|
+
if (JSON.stringify(this.doc.sessions.find(s => s.session_id === id)) !== JSON.stringify(record)) {
|
|
274
|
+
report.target_confirmed = false;
|
|
275
|
+
report.target = null;
|
|
276
|
+
report.code = 'SESSION_CLOSE_TARGET_CHANGED';
|
|
277
|
+
}
|
|
278
|
+
return { session_id: id, operation_id: record.operation_id, generation: record.generation, close_diagnostics: report };
|
|
279
|
+
}
|
|
266
280
|
assertRuntimeMessage(id) {
|
|
267
281
|
const record = this.doc.sessions.find(s => s.session_id === id);
|
|
268
282
|
if (this.closingOperations.has(id))
|
|
@@ -77,6 +77,7 @@ import { CollaborationError } from '../agent/collaborationProtocol.js';
|
|
|
77
77
|
import { resolveCollaborationBackend, resolveCollaborationSessionId } from '../agent/sessionBindingRecovery.js';
|
|
78
78
|
import { CollaborationRoutingStore, selectCollaborationPane, selectDrivePane, assessCollaborationRouteIdentity } from '../agent/collaborationRouting.js';
|
|
79
79
|
import { selectRecoveredIntegrationBinding } from '../agent/integrationRecoveryBinding.js';
|
|
80
|
+
import { assertDetachedCloseOwnership } from '../agent/integrationCloseOwnership.js';
|
|
80
81
|
import { readTmuxLayout, readTmuxLayouts } from '../utils/tmuxLayoutReader.js';
|
|
81
82
|
import { CollaborationDeliveryWorker } from '../agent/collaborationDeliveryWorker.js';
|
|
82
83
|
import { approveCollaborationDialog, captureTmuxPaneHistory, captureTmuxPaneText, sendTmuxPaneKey, writeCollaborationTmuxPane } from '../agent/collaborationTmuxDelivery.js';
|
|
@@ -2158,8 +2159,8 @@ function integrationRuntimeRegistered(record) {
|
|
|
2158
2159
|
async function verifyIntegrationRuntimeOwnership(record) {
|
|
2159
2160
|
await integrationSessionAdapter.prepareRelease(record);
|
|
2160
2161
|
const terminal = integrationTerminalRecord(record);
|
|
2161
|
-
if (
|
|
2162
|
-
&& (other.tmuxSessionName ===
|
|
2162
|
+
if (globalSessionState.sessions.some(other => other.sessionId !== record.session_id
|
|
2163
|
+
&& (other.tmuxSessionName === `wt-integration-${record.session_id}` || terminal?.backendSessionId && other.backendSessionId === terminal.backendSessionId))) {
|
|
2163
2164
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Runtime has another logical owner', 409);
|
|
2164
2165
|
}
|
|
2165
2166
|
}
|
|
@@ -2179,14 +2180,36 @@ async function forgetIntegrationRuntime(record) {
|
|
|
2179
2180
|
async function observeIntegrationCloseTarget(record) {
|
|
2180
2181
|
await verifyIntegrationRuntimeOwnership(record);
|
|
2181
2182
|
const terminal = integrationTerminalRecord(record);
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2183
|
+
const detached = !terminal;
|
|
2184
|
+
const sessionName = terminal?.tmuxSessionName ?? `wt-integration-${record.session_id}`;
|
|
2185
|
+
let backendSessionId = terminal?.backendSessionId ?? null;
|
|
2186
|
+
if (!record.terminal_binding)
|
|
2187
|
+
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Original persisted pane binding is unavailable', 409);
|
|
2188
|
+
if (!terminal) {
|
|
2189
|
+
if (process.platform !== 'linux' || !record.agent_native_session_id)
|
|
2190
|
+
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Missing terminal registration requires a current Linux writer-lock native identity', 409);
|
|
2191
|
+
const owners = await integrationRecoveryOwnership(record);
|
|
2192
|
+
collaborationRouting.assertReadable();
|
|
2193
|
+
const backends = [...terminalSessions].filter(([, backend]) => backend.mode === 'tmux' && backend.tmuxSessionName === sessionName);
|
|
2194
|
+
const group = collaborationStore.getGroup(record.group_id);
|
|
2195
|
+
assertDetachedCloseOwnership({ sessionId: record.session_id, groupId: record.group_id, principalId: record.principal_id,
|
|
2196
|
+
...owners, localGroup: !!group && !group.deleted && !group.federated,
|
|
2197
|
+
groupOwners: collaborationStore.groupsForSession(record.session_id).map(g => g.id),
|
|
2198
|
+
terminalClaims: globalSessionState.sessions.filter(s => s.sessionId === record.session_id || s.tmuxSessionName === sessionName).length,
|
|
2199
|
+
backendClaims: backends.map(([id]) => ({ routeOwner: collaborationRouting.ownerOfBackend(id),
|
|
2200
|
+
foreignTerminal: globalSessionState.sessions.some(s => s.backendSessionId === id && s.sessionId !== record.session_id) })),
|
|
2201
|
+
otherPaneOwner: collaborationRouting.hasOtherPaneOwner(record.session_id, record.terminal_binding),
|
|
2202
|
+
});
|
|
2203
|
+
// Local observation context only: never insert/adopt a global terminal or
|
|
2204
|
+
// backend, revive a released session, or alter delivery routing.
|
|
2205
|
+
backendSessionId = backends[0]?.[0] ?? null;
|
|
2206
|
+
}
|
|
2207
|
+
const inspected = await inspectCollaborationTmux({ sessionId: record.session_id, backendSessionId,
|
|
2208
|
+
mode: 'tmux', tmuxSessionName: sessionName, agentSlug: record.agent_slug,
|
|
2209
|
+
nativeSessionId: record.agent_native_session_id, pane: record.terminal_binding }, undefined, false, true, true, false, integrationNativeIdentity(record));
|
|
2187
2210
|
if (inspected.state !== 'ready' || !inspected.pane)
|
|
2188
2211
|
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Original pinned pane cannot be verified', 409);
|
|
2189
|
-
const layout = await getTmuxLayout(
|
|
2212
|
+
const layout = await getTmuxLayout(sessionName);
|
|
2190
2213
|
const panes = layout.windows.flatMap(window => window.panes);
|
|
2191
2214
|
if (panes.length !== 1 || layout.activePaneId !== record.terminal_binding.paneId)
|
|
2192
2215
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Only the single original owned pane can be closed', 409);
|
|
@@ -2206,11 +2229,15 @@ async function observeIntegrationCloseTarget(record) {
|
|
|
2206
2229
|
const identity = await observeProgramNativeIdentity(agent, program, integrationNativeIdentity(record));
|
|
2207
2230
|
if (identity.failure || identity.nativeId !== record.agent_native_session_id)
|
|
2208
2231
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Current Agent native identity differs from the owned record', 409);
|
|
2232
|
+
if (detached && identity.source !== 'linux-flock-owner')
|
|
2233
|
+
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Missing terminal registration requires independent live writer-lock proof, not argv or a cached UUID', 409);
|
|
2209
2234
|
kind = 'agent';
|
|
2210
2235
|
pid = program.processPid;
|
|
2211
2236
|
nativeId = identity.nativeId;
|
|
2212
2237
|
}
|
|
2213
2238
|
else {
|
|
2239
|
+
if (detached)
|
|
2240
|
+
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Missing terminal registration cannot authorize closing an unproven shell or another program', 409);
|
|
2214
2241
|
if (!await integrationPaneIsIdleShell(inspected.pane))
|
|
2215
2242
|
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'Current runtime is not a verified foreground shell', 409);
|
|
2216
2243
|
kind = 'shell';
|
|
@@ -2251,6 +2278,25 @@ export const integrationSessionAdapter = {
|
|
|
2251
2278
|
await verifyIntegrationRuntimeOwnership(record);
|
|
2252
2279
|
return await integrationTmuxPresent(record) ? observeIntegrationCloseTarget(record) : null;
|
|
2253
2280
|
},
|
|
2281
|
+
async closeDiagnostics(record) {
|
|
2282
|
+
const report = { checked_at: Date.now(), terminal_registration_present: false, binding_present: !!record.terminal_binding,
|
|
2283
|
+
runtime_present: null, target_confirmed: false, pending_messages: collaborationStore.pendingCount(record.session_id),
|
|
2284
|
+
code: null, target: null };
|
|
2285
|
+
try {
|
|
2286
|
+
report.terminal_registration_present = !!integrationTerminalRecord(record);
|
|
2287
|
+
report.runtime_present = await integrationSessionAdapter.runtimePresent(record);
|
|
2288
|
+
if (await integrationTmuxPresent(record)) {
|
|
2289
|
+
report.target = await observeIntegrationCloseTarget(record);
|
|
2290
|
+
report.target_confirmed = true;
|
|
2291
|
+
}
|
|
2292
|
+
else
|
|
2293
|
+
report.code = report.runtime_present ? 'SESSION_CLOSE_TARGET_UNCONFIRMED' : 'SESSION_RUNTIME_ABSENT';
|
|
2294
|
+
}
|
|
2295
|
+
catch (error) {
|
|
2296
|
+
report.code = typeof error.code === 'string' ? error.code : 'SESSION_CLOSE_OBSERVATION_FAILED';
|
|
2297
|
+
}
|
|
2298
|
+
return report;
|
|
2299
|
+
},
|
|
2254
2300
|
async closeRuntime(record, reconcile) {
|
|
2255
2301
|
await closeSessionRuntime({
|
|
2256
2302
|
barrier: action => collaborationDeliveryWorker.reconfigure(record.session_id, action),
|
|
@@ -2352,8 +2398,11 @@ export const integrationSessionAdapter = {
|
|
|
2352
2398
|
},
|
|
2353
2399
|
async inspect(record) {
|
|
2354
2400
|
const terminal = integrationTerminalRecord(record);
|
|
2355
|
-
if (!terminal)
|
|
2401
|
+
if (!terminal) {
|
|
2402
|
+
if (await integrationTmuxPresent(record))
|
|
2403
|
+
integrationError('SESSION_TERMINAL_REGISTRATION_MISSING', 'Owned tmux exists but its active TD terminal registration is missing; use administrator close-diagnostics', 409);
|
|
2356
2404
|
return { exists: false, running: false, shell: false, agentSlug: null, nativeId: null };
|
|
2405
|
+
}
|
|
2357
2406
|
const binding = { sessionId: record.session_id, backendSessionId: terminal.backendSessionId,
|
|
2358
2407
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: record.agent_slug,
|
|
2359
2408
|
nativeSessionId: record.agent_native_session_id, pane: record.terminal_binding ?? null };
|
package/docs/integrations.md
CHANGED
|
@@ -1411,3 +1411,63 @@ wrapper 负责实际设置 Agent 自己的数据目录、准备 hooks/参数,
|
|
|
1411
1411
|
回归覆盖重复 ACK 零克隆/编码/写入、真实持久提交与中途写入/fsync 失败后重试、短写与 Unicode、64 MiB 精确字节上限、输入/输出不可修改历史、分页/保留缺口/撤权、推送事实等价,以及隔离真实 tmux 的多窗口/分屏/激活 pane/copy-mode 与 client/PID/尺寸不变。Linux 维护机验证不代替 macOS 实机或私有 Agent 的业务验收。生产升级须与共享服务的在途任务和维护窗协调,不能因为收到版本通知就自动重启别人的实验。
|
|
1412
1412
|
|
|
1413
1413
|
同时修复独立 profile 目录的 home 别名误拒:当逻辑 home 指向真实 home,规范化 cwd 下的 `{cwd}` 路径可匹配逻辑 home 或 `realpath(home)` 的严格子目录;不会将同名前缀兄弟目录视为 home,不创建尚不存在的锁目录。实际 provider 继续核验 canonicalHome、目录权限、精确进程实例、FD/inode 和内核持锁关系。Linux capabilities 新增 `session_profile_native_identity_home_aliases=true` 及 `session_profile_native_identity_home_aliases_min_version="1.4.348"`。旧 profile 快照不迁移,不自动 retry/create;347 的 `~/` 每 job 精确路径声明仍可沿原契约使用,更新同一 profile 与捕获快照需要由接入方串行协调。
|
|
1414
|
+
|
|
1415
|
+
## 1.4.349:活动终端登记缺失时的受限关闭与只读诊断
|
|
1416
|
+
|
|
1417
|
+
问题是三个不同的事实层可能不一致:持久 IntegrationSession 仍有原 native UUID 与完整 pane 绑定,tmux/Agent 仍存在,但全局活动终端登记已经缺失。348 的 close 先依赖该登记,因而在真实身份核验之前拒绝;inspect 也可能将登记缺失表现为 `AGENT_NOT_RUNNING`。349 对仍存在 tmux 的缺登记观察改报 `SESSION_TERMINAL_REGISTRATION_MISSING`,不据此推断程序退出。这不解释或修复所有登记丢失的起因,也不自动重建 UI 记录、恢复会话或认领未知进程。
|
|
1418
|
+
|
|
1419
|
+
最低 CLI / 实际服务 **1.4.349**,管理员和所属 principal capabilities:
|
|
1420
|
+
|
|
1421
|
+
- `session_close_diagnostics=true`、`session_close_diagnostics_min_version="1.4.349"`。
|
|
1422
|
+
- Linux 上 `session_close_detached_registration=true`、`session_close_detached_registration_min_version="1.4.349"`;其它平台 false。没有新增 principal 权限,关闭仍需原 `session.close`,诊断仅管理员。
|
|
1423
|
+
|
|
1424
|
+
管理员正式只读入口:
|
|
1425
|
+
|
|
1426
|
+
```sh
|
|
1427
|
+
td integration close-diagnostics <原TD-session-id>
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
走原本机管理员 Unix socket,`GET /admin/sessions/:id/close-diagnostics`,返回:
|
|
1431
|
+
|
|
1432
|
+
```json
|
|
1433
|
+
{
|
|
1434
|
+
"session_id": "原逻辑ID",
|
|
1435
|
+
"operation_id": "当前operation",
|
|
1436
|
+
"generation": 2,
|
|
1437
|
+
"close_diagnostics": {
|
|
1438
|
+
"checked_at": 1790000000000,
|
|
1439
|
+
"terminal_registration_present": false,
|
|
1440
|
+
"binding_present": true,
|
|
1441
|
+
"runtime_present": true,
|
|
1442
|
+
"pending_messages": 0,
|
|
1443
|
+
"target_confirmed": true,
|
|
1444
|
+
"code": null,
|
|
1445
|
+
"target": {
|
|
1446
|
+
"binding": {"serverPid": 101, "sessionId": "$2", "paneId": "%2", "panePid": 102, "agentSlug": "已登记Agent", "nativeSessionId": "原UUID"},
|
|
1447
|
+
"tmux_server_start_id": "linux:1000",
|
|
1448
|
+
"kind": "agent",
|
|
1449
|
+
"process_pid": 103,
|
|
1450
|
+
"process_start_id": "linux:1001",
|
|
1451
|
+
"native_id": "原UUID"
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
这是字段示例,不代表某一现场已核验。诊断不调用 session get/refresh、不改变 descriptor/代次/路由/组,不启动或终止进程、不输入按键。`target` 只包含原 pane、当前进程实例和已证明 UUID,不读/返回 argv、环境或终端正文。记录在异步观察期间变化时丢弃 target,返回 `SESSION_CLOSE_TARGET_CHANGED`。`target_confirmed` 只表示本次目标核验通过,不等于后续关闭授权/成功;pending、进行中生命周期、当前代次等仍由真正 close 独立检查。观察失败保留 `target=null` 和准确 code;无法观察运行存在性时为 null,不假装已消失。已有 `td integration diagnostics` 仍只读原 launch/restore 持久诊断。
|
|
1458
|
+
|
|
1459
|
+
缺活动登记时,现有显式 `session close` 只在以下证据全部满足后工作:
|
|
1460
|
+
|
|
1461
|
+
1. 全机持久 IntegrationSession 只有一个 descriptor 声明该 session/principal/组;principal 未撤销且归属唯一;当前本机组仍存在且该成员只归属此组。组的 archived 显示元数据不影响此核验;缺组、跨服务组、重复/跨组归属拒绝。
|
|
1462
|
+
2. 无其它全局终端声明该逻辑 ID 或保留 tmux 名,无重复 backend、其它逻辑 owner 或冲突 route/pane owner;routing 不可读也拒绝。
|
|
1463
|
+
3. 保留名精确为 `wt-integration-<原ID>`,实时 server/session/pane/PID 全部等于持久原绑定,只有一个 pane,且未链接到其它 tmux session。不是仅凭名称、cwd、显示名、短 pane ID 或 PID 存在认领。
|
|
1464
|
+
4. 当前实际前台是原 Agent 类型,当前 Linux `linux-flock-owner` provider 独立证明原 UUID,并核验进程 starttime、FD/inode、权限及 kernel flock。argv/resume UUID、旧缓存或单纯 shell 观察不能给缺登记会话授权关闭;未安装相应只读身份 provider 时继续拒绝,不扩大为任意 Agent 黑盒支持。
|
|
1465
|
+
5. 沿原生命周期串行、worker 屏障、pending 检查、generation/CAS 和稳定幂等键,先持久化 close intent,再重新核验完整目标与进程实例,使用不可变 tmux session ID 终止一次。失败/重启结果不明沿现有 close 对账,不重复终止。并发外部 tmux 操作仍有系统级竞争边界,连续核验不能宣称绝对原子。
|
|
1466
|
+
|
|
1467
|
+
无活动登记不再单独阻止已充分证明的目标关闭;未提供 force/裸 UUID/按名清理入口。没有补建 active terminal、backend 或网页客户端,没有修改旧 profile/启动配置、投递正文、重放消息或重新打开归档组。`closed` 仍保留逻辑记录、诊断、消息、工作目录和原生历史并拒绝 send/reply/restore;不是 release。
|
|
1468
|
+
|
|
1469
|
+
现场接入先升级并核验 **实际服务** 两项 capability,再对原 ID 运行管理员 close-diagnostics。确认归属与 `target_confirmed` 后,由原 principal 正式 get 读当前 generation;若仍与原请求一致,沿原 generation/原 key 重试原 close。若 generation 已改变,先对账原请求,不擅自换 key;无历史 intent 的新关闭需据新代次明确生成新操作键。最终 get 必须确认 `closed/runtime_present=false/terminal_binding=null/close_pending=false`,并核对旧 pane/进程确已消失;诊断通过或 API 请求成功本身不代替回收验证。
|
|
1470
|
+
|
|
1471
|
+
错误包括 `SESSION_CLOSE_OWNERSHIP_UNCONFIRMED`(持久 owner/本机组不唯一或缺失)、`SESSION_IDENTITY_MISMATCH`(其它声明或实际 native 冲突)、`SESSION_CLOSE_TARGET_UNCONFIRMED`(原绑定/实时 writer-lock/前台实例不足)、`SESSION_CLOSE_TARGET_CHANGED`、`SESSION_PENDING_MESSAGES` 及原有代次/权限/生命周期错误。管理员报告不能越过这些检查。
|
|
1472
|
+
|
|
1473
|
+
回归包含唯一及重复/撤销/跨组/跨服务归属、缺登记精确关闭、argv-only/错误 UUID/换 pane/换进程拒绝、缺登记准确错误、诊断无副作用及代次变化丢弃、Unix 管理权限与 CLI capability 门控、原关闭意图/幂等/重启和失败 shell 兼容;隔离真实 tmux 验证只终止精确目标并保留邻接会话。该 tmux 测试用可控身份夹具,Linux 原生 writer-lock provider 另有真实内核/FD/starttime 回归;私有 TraeX 和上述远端目标仍须由接入方在维护窗实机验收,不以合成测试冒充。
|
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.349",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-ObG1OGq7e7fMFQmYoPxPp3jRodEusYHulVNa5mT1aW8=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-2vXkNt6WtThYWY+7lOPI2qTULTqt2YLY6DEiMlTBb7k=",
|
|
10
10
|
"clientBundleHash": "sha256-kn3Tg+BTj+H4rZe0g9OE5JUEQNsBGiUSGew8Epm5A38=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|