@ai-workbench/remote-agent 0.0.0-stage → 0.8.1
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 +58 -2
- package/cordis.patch.yml +3 -0
- package/dist/acp.d.ts +54 -0
- package/dist/acp.js +341 -0
- package/dist/agents.d.ts +15 -0
- package/dist/agents.js +4 -0
- package/dist/broker.d.ts +67 -0
- package/dist/broker.js +397 -0
- package/dist/client.js +36 -0
- package/dist/connect-contract.d.ts +26 -0
- package/dist/connect-contract.js +1 -0
- package/dist/conversation-details.d.ts +47 -0
- package/dist/conversation-details.js +319 -0
- package/dist/host-layout.d.ts +8 -0
- package/dist/host-layout.js +9 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +360 -0
- package/dist/input.d.ts +12 -0
- package/dist/input.js +49 -0
- package/dist/interaction.d.ts +19 -0
- package/dist/interaction.js +1 -0
- package/dist/job.d.ts +17 -0
- package/dist/job.js +57 -0
- package/dist/mapping.d.ts +46 -0
- package/dist/mapping.js +230 -0
- package/dist/oneshot.d.ts +13 -0
- package/dist/oneshot.js +78 -0
- package/dist/presentation.d.ts +6 -0
- package/dist/presentation.js +45 -0
- package/dist/provider-locale.d.ts +19 -0
- package/dist/provider-locale.js +73 -0
- package/dist/provider.d.ts +144 -0
- package/dist/provider.js +164 -0
- package/dist/readiness.d.ts +28 -0
- package/dist/readiness.js +192 -0
- package/dist/remote-details.d.ts +26 -0
- package/dist/remote-details.js +250 -0
- package/dist/remote.d.ts +34 -0
- package/dist/remote.js +51 -0
- package/dist/sdk-ssh-wrapper.d.ts +2 -0
- package/dist/sdk-ssh-wrapper.js +34 -0
- package/dist/workspace-notice.d.ts +47 -0
- package/dist/workspace-notice.js +273 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +97 -4
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute, join, parse } from 'node:path';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import { sanitizeObservation, isSanitizedRedaction } from "./conversation-details.js";
|
|
7
|
+
import { NeedError } from "./remote.js";
|
|
8
|
+
const failure = (code, message = '技术详情未能持久记录,本轮已停止。') => new NeedError(code, message, '可用的远程主机连接技术详情');
|
|
9
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
10
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
function unwrap(result) {
|
|
12
|
+
if (!object(result))
|
|
13
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
14
|
+
if (result.ok === false && object(result.error) && typeof result.error.code === 'string')
|
|
15
|
+
throw failure(result.error.code);
|
|
16
|
+
if (result.ok !== true)
|
|
17
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
18
|
+
return result.value;
|
|
19
|
+
}
|
|
20
|
+
function identifier(value) { return value === null || typeof value === 'string' && value.trim().length > 0 || typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
|
|
21
|
+
function kind(observation) {
|
|
22
|
+
if (/UNKNOWN|UNSUPPORTED|FOREIGN/.test(observation.code))
|
|
23
|
+
return observation.severity === 'action-required' ? 'action-required' : 'unknown';
|
|
24
|
+
return { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind;
|
|
25
|
+
}
|
|
26
|
+
async function privateDirectory(path) {
|
|
27
|
+
if (!isAbsolute(path))
|
|
28
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
29
|
+
// Existing parent/profile ownership is the host's responsibility; reject symlinks
|
|
30
|
+
// along the full path before creating our own private child directories.
|
|
31
|
+
let part = parse(path).root;
|
|
32
|
+
for (const member of path.slice(part.length).split('/').filter(Boolean)) {
|
|
33
|
+
part = join(part, member);
|
|
34
|
+
try {
|
|
35
|
+
const info = await lstat(part);
|
|
36
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
37
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error?.code !== 'ENOENT')
|
|
41
|
+
throw error;
|
|
42
|
+
try {
|
|
43
|
+
await mkdir(part, { mode: 0o700 });
|
|
44
|
+
}
|
|
45
|
+
catch (creation) {
|
|
46
|
+
if (creation?.code !== 'EEXIST')
|
|
47
|
+
throw creation;
|
|
48
|
+
const info = await lstat(part);
|
|
49
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
50
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (await realpath(path) !== path)
|
|
55
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
56
|
+
const info = await lstat(path);
|
|
57
|
+
if ((info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
|
|
58
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
59
|
+
}
|
|
60
|
+
async function durableWrite(path, value) {
|
|
61
|
+
const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
62
|
+
try {
|
|
63
|
+
await file.writeFile(JSON.stringify(value) + '\n');
|
|
64
|
+
await file.sync();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await file.close();
|
|
68
|
+
}
|
|
69
|
+
const parent = await open(join(path, '..'), 'r');
|
|
70
|
+
try {
|
|
71
|
+
await parent.sync();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await parent.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function readJournal(path) {
|
|
78
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
79
|
+
try {
|
|
80
|
+
const info = await file.stat();
|
|
81
|
+
if (!info.isFile() || (info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
|
|
82
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
83
|
+
return JSON.parse(await file.readFile('utf8'));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await file.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function createRemoteDetails(options) {
|
|
90
|
+
const root = join(options.profileDir, 'ai-workbench-remote-agent', 'diagnostics');
|
|
91
|
+
const scopes = new Map();
|
|
92
|
+
const initialize = async () => { await privateDirectory(join(options.profileDir, 'ai-workbench-remote-agent')); await privateDirectory(root); };
|
|
93
|
+
return { async open(request) {
|
|
94
|
+
if (!request.sessionId || !request.identity || !request.connection.instanceId || !Number.isSafeInteger(request.connection.epoch) || request.connection.epoch < 0 || !['claude', 'codex'].includes(request.provider))
|
|
95
|
+
throw failure('REMOTE_DETAIL_SCOPE_INVALID');
|
|
96
|
+
try {
|
|
97
|
+
await initialize();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
101
|
+
}
|
|
102
|
+
const api = options.getApi();
|
|
103
|
+
const stable = () => {
|
|
104
|
+
const now = api?.getConnectionState();
|
|
105
|
+
if (!api || options.getApi() !== api || now?.phase !== 'connected' || now.instanceId !== request.connection.instanceId || now.epoch !== request.connection.epoch)
|
|
106
|
+
throw failure('CONNECTION_LOST');
|
|
107
|
+
};
|
|
108
|
+
stable();
|
|
109
|
+
const capability = api.getContractInfo?.();
|
|
110
|
+
if (!object(capability) || capability.remoteConversationDetails !== 1 || !['appendRemoteConversationDetail', 'getRemoteConversationDetailReceipt', 'listRemoteConversationDetails'].every(key => typeof api[key] === 'function')) {
|
|
111
|
+
await durableWrite(join(root, randomUUID() + '.json'), { version: 1, code: 'TECHNICAL_DETAILS_UNAVAILABLE', summary: '技术详情暂不可用', session: hash(request.sessionId), provider: request.provider });
|
|
112
|
+
throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
|
|
113
|
+
}
|
|
114
|
+
const key = hash(request.identity + '\0' + request.sessionId + '\0' + request.provider), dir = join(root, key);
|
|
115
|
+
await privateDirectory(dir);
|
|
116
|
+
const scope = scopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
|
|
117
|
+
scopes.set(key, scope);
|
|
118
|
+
// Serialize open and appends in this host-user/session/brand scope.
|
|
119
|
+
const load = scope.tail.then(async () => {
|
|
120
|
+
stable();
|
|
121
|
+
const generation = request.connection.instanceId + '\0' + request.connection.epoch;
|
|
122
|
+
if (scope.loaded && scope.generation === generation)
|
|
123
|
+
return;
|
|
124
|
+
const names = await readdir(dir);
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
|
|
127
|
+
if (!match)
|
|
128
|
+
continue;
|
|
129
|
+
const n = Number(match[1]);
|
|
130
|
+
if (!Number.isSafeInteger(n))
|
|
131
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
132
|
+
scope.maximum = Math.max(scope.maximum, n);
|
|
133
|
+
const saved = await readJournal(join(dir, name));
|
|
134
|
+
if (!object(saved) || !object(saved.detail) || saved.detail.eventId !== match[2] || saved.detail.sequence !== n || saved.detail.sessionId !== request.sessionId || saved.detail.provider !== request.provider)
|
|
135
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
136
|
+
const settled = 'settled-' + match[2] + '.json';
|
|
137
|
+
if (names.includes(settled)) {
|
|
138
|
+
const result = await readJournal(join(dir, settled));
|
|
139
|
+
if (!object(result) || result.eventId !== match[2] || !['recorded', 'rejected'].includes(result.state))
|
|
140
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// An interrupted append is reconciled by pure read, with the original
|
|
144
|
+
// event's epoch intact. Missing receipts stay unknown and never trigger append.
|
|
145
|
+
stable();
|
|
146
|
+
const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: match[2], sessionId: request.sessionId, provider: request.provider }));
|
|
147
|
+
stable();
|
|
148
|
+
if (receipt.eventId !== match[2])
|
|
149
|
+
throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
|
|
150
|
+
if (!receipt.recorded)
|
|
151
|
+
throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
|
|
152
|
+
if (!isDeepStrictEqual(receipt.detail, saved.detail))
|
|
153
|
+
throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
|
|
154
|
+
await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
|
|
155
|
+
}
|
|
156
|
+
let cursor, previous = -1;
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
let expected;
|
|
159
|
+
let count = 0;
|
|
160
|
+
do {
|
|
161
|
+
stable();
|
|
162
|
+
const page = unwrap(await api.listRemoteConversationDetails({ sessionId: request.sessionId, provider: request.provider, ...(cursor ? { cursor } : {}) }));
|
|
163
|
+
stable();
|
|
164
|
+
if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
|
|
165
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
166
|
+
const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
|
|
167
|
+
if (expected && !isDeepStrictEqual(expected, counts))
|
|
168
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
169
|
+
expected = counts;
|
|
170
|
+
for (const entry of page.items) {
|
|
171
|
+
if (entry.sessionId !== request.sessionId || entry.provider !== request.provider || !Number.isSafeInteger(entry.sequence) || entry.sequence <= previous)
|
|
172
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
173
|
+
previous = entry.sequence;
|
|
174
|
+
scope.maximum = Math.max(scope.maximum, entry.sequence);
|
|
175
|
+
count++;
|
|
176
|
+
}
|
|
177
|
+
cursor = page.nextCursor;
|
|
178
|
+
if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
|
|
179
|
+
throw failure('REMOTE_DETAIL_CURSOR_INVALID');
|
|
180
|
+
if (cursor)
|
|
181
|
+
seen.add(cursor);
|
|
182
|
+
} while (cursor);
|
|
183
|
+
if (count !== expected?.total)
|
|
184
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
185
|
+
scope.loaded = true;
|
|
186
|
+
scope.generation = generation;
|
|
187
|
+
});
|
|
188
|
+
scope.tail = load.then(() => { }, () => { });
|
|
189
|
+
await load;
|
|
190
|
+
let turn = null, step = null;
|
|
191
|
+
let tail = Promise.resolve(), firstFailure;
|
|
192
|
+
const enqueue = (operation) => {
|
|
193
|
+
const task = scope.tail.then(operation);
|
|
194
|
+
scope.tail = task.then(() => { }, () => { });
|
|
195
|
+
tail = task.catch(error => { scope.loaded = false; firstFailure ??= { error }; });
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
beginTurn(ids) { if (!identifier(ids.turn) || !identifier(ids.step))
|
|
199
|
+
throw failure('REMOTE_DETAIL_SCOPE_INVALID'); turn = ids.turn; step = ids.step; },
|
|
200
|
+
endTurn() { turn = null; step = null; },
|
|
201
|
+
record(observation) {
|
|
202
|
+
const safe = sanitizeObservation(observation, true);
|
|
203
|
+
const wire = (value) => Array.isArray(value) ? value.map(wire) : object(value) ? isSanitizedRedaction(value) ? (typeof value.valueType === 'string' ? { redacted: true, valueType: value.valueType, value: '已脱敏' } : '已脱敏') : Object.fromEntries(Object.entries(value).map(([key, child]) => [key, wire(child)])) : value;
|
|
204
|
+
const sequence = scope.maximum + 1;
|
|
205
|
+
if (!Number.isSafeInteger(sequence))
|
|
206
|
+
throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
|
|
207
|
+
scope.maximum = sequence;
|
|
208
|
+
const detail = Object.freeze({ ...safe, payload: wire(safe.payload), version: 1, eventId: randomUUID(), instanceId: request.connection.instanceId, epoch: request.connection.epoch, sessionId: request.sessionId, provider: request.provider, turn, step, sequence, kind: kind(observation) });
|
|
209
|
+
enqueue(async () => {
|
|
210
|
+
// A private X journal is retained even if L is disconnected or rejects it.
|
|
211
|
+
const file = join(dir, sequence + '-' + detail.eventId + '.json');
|
|
212
|
+
try {
|
|
213
|
+
await durableWrite(file, { state: 'pending', detail });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
217
|
+
}
|
|
218
|
+
stable();
|
|
219
|
+
let response;
|
|
220
|
+
try {
|
|
221
|
+
response = await api.appendRemoteConversationDetail(structuredClone(detail));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
stable();
|
|
225
|
+
const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
|
|
226
|
+
stable();
|
|
227
|
+
if (receipt.eventId !== detail.eventId)
|
|
228
|
+
throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
|
|
229
|
+
if (!receipt.recorded)
|
|
230
|
+
throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
|
|
231
|
+
if (!isDeepStrictEqual(receipt.detail, detail))
|
|
232
|
+
throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
|
|
233
|
+
await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
stable();
|
|
237
|
+
if (response?.ok === false) {
|
|
238
|
+
await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
|
|
239
|
+
}
|
|
240
|
+
const ack = unwrap(response);
|
|
241
|
+
if (ack.eventId !== detail.eventId || ack.recorded !== true)
|
|
242
|
+
throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
|
|
243
|
+
await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
async flush() { await tail; if (firstFailure)
|
|
247
|
+
throw firstFailure.error; stable(); },
|
|
248
|
+
};
|
|
249
|
+
} };
|
|
250
|
+
}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
/** Development-only direct SSH. Product transports must consume L's exported sshArgs (v0.2). */
|
|
3
|
+
export interface DevelopmentSshTarget {
|
|
4
|
+
kind: 'development-only';
|
|
5
|
+
host: string;
|
|
6
|
+
username: string;
|
|
7
|
+
port: number;
|
|
8
|
+
proxyCommand?: string;
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Need {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
requires: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class NeedError extends Error {
|
|
17
|
+
code: string;
|
|
18
|
+
requires: string;
|
|
19
|
+
constructor(code: string, message: string, requires: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function shellQuote(value: string): string;
|
|
22
|
+
export declare function sshArgs(target: DevelopmentSshTarget, command: string, noInput: boolean): string[];
|
|
23
|
+
export declare function openSsh(target: DevelopmentSshTarget, command: string, noInput?: boolean): ChildProcessWithoutNullStreams;
|
|
24
|
+
export interface Captured {
|
|
25
|
+
code: number | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
}
|
|
29
|
+
export type Capture = (target: DevelopmentSshTarget, command: string) => Promise<Captured>;
|
|
30
|
+
export declare function capture(target: DevelopmentSshTarget, command: string): Promise<Captured>;
|
|
31
|
+
export declare function checkCodexLogin(target: DevelopmentSshTarget, execute?: Capture): Promise<{
|
|
32
|
+
loggedIn: boolean;
|
|
33
|
+
need?: Need;
|
|
34
|
+
}>;
|
package/dist/remote.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class NeedError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
requires;
|
|
5
|
+
constructor(code, message, requires) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'NeedError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.requires = requires;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function shellQuote(value) {
|
|
13
|
+
if (value.includes('\0'))
|
|
14
|
+
throw new NeedError('INVALID_ARGUMENT', '远端参数含无效字符', '有效的远端参数');
|
|
15
|
+
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
16
|
+
}
|
|
17
|
+
export function sshArgs(target, command, noInput) {
|
|
18
|
+
if (target.kind !== 'development-only' || !/^[a-zA-Z0-9][a-zA-Z0-9.:-]*$/.test(target.host)
|
|
19
|
+
|| !/^emp-[a-z0-9-]{1,28}$/.test(target.username)
|
|
20
|
+
|| !Number.isInteger(target.port) || target.port < 1 || target.port > 65535
|
|
21
|
+
|| target.proxyCommand?.includes('\0')) {
|
|
22
|
+
throw new NeedError('INVALID_TARGET', '我需要 connect 插件提供有效的员工 SSH 目标', 'connect-api SSH 目标');
|
|
23
|
+
}
|
|
24
|
+
return [...(noInput ? ['-n'] : []), '-o', 'BatchMode=yes',
|
|
25
|
+
...(target.proxyCommand ? ['-o', `ProxyCommand=${target.proxyCommand}`] : []),
|
|
26
|
+
'-p', String(target.port), '-l', target.username, target.host, command];
|
|
27
|
+
}
|
|
28
|
+
export function openSsh(target, command, noInput = false) {
|
|
29
|
+
return spawn('ssh', sshArgs(target, command, noInput), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
30
|
+
}
|
|
31
|
+
export async function capture(target, command) {
|
|
32
|
+
const child = openSsh(target, command, true);
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
child.stdout.setEncoding('utf8').on('data', part => { stdout += part; });
|
|
36
|
+
child.stderr.setEncoding('utf8').on('data', part => { stderr += part; });
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
child.on('error', () => reject(new NeedError('SSH_FAILED', 'SSH 启动失败,根因未查明', '可用的 SSH 连接')));
|
|
39
|
+
child.on('close', code => resolve({ code, stdout, stderr }));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export async function checkCodexLogin(target, execute = capture) {
|
|
43
|
+
const { code } = await execute(target, 'codex login status');
|
|
44
|
+
if (code === 0)
|
|
45
|
+
return { loggedIn: true };
|
|
46
|
+
if (code === 1)
|
|
47
|
+
return { loggedIn: false, need: { code: 'LOGIN_REQUIRED', message: '我需要该员工在主机上的 Codex 完成登录,请通过连接插件授权。', requires: '该员工自己的 Codex 登录' } };
|
|
48
|
+
if (code === 255 || code === null)
|
|
49
|
+
throw new NeedError('SSH_FAILED', 'SSH 检查失败,根因未查明', '可用的员工 SSH 连接');
|
|
50
|
+
throw new NeedError('AUTH_STATUS_INVALID', 'Codex 登录状态检查异常,根因未查明', '有效的官方登录状态');
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Codex SDK executable override. Prompt travels through stdin, never SSH argv. */
|
|
3
|
+
import { readFile, lstat } from 'node:fs/promises';
|
|
4
|
+
import { executeJob } from "./job.js";
|
|
5
|
+
let child;
|
|
6
|
+
let stopped = false;
|
|
7
|
+
const lifetime = new AbortController();
|
|
8
|
+
for (const sig of ['SIGTERM', 'SIGINT'])
|
|
9
|
+
process.on(sig, () => { stopped = true; lifetime.abort(); child?.kill('SIGTERM'); });
|
|
10
|
+
try {
|
|
11
|
+
const path = process.env.AW_X_JOB_FILE;
|
|
12
|
+
if (!path)
|
|
13
|
+
throw new Error('job missing');
|
|
14
|
+
const meta = await lstat(path);
|
|
15
|
+
if (!meta.isFile() || meta.isSymbolicLink() || meta.uid !== process.getuid?.() || (meta.mode & 0o077) !== 0)
|
|
16
|
+
throw new Error('job permissions');
|
|
17
|
+
const spec = JSON.parse(await readFile(path, 'utf8'));
|
|
18
|
+
let input = '';
|
|
19
|
+
for await (const chunk of process.stdin)
|
|
20
|
+
input += chunk;
|
|
21
|
+
if (!stopped) {
|
|
22
|
+
const code = await executeJob(spec, 'stage', JSON.stringify({ argv: process.argv.slice(2), input, cwd: spec.cwd, codex: spec.codex }), c => { child = c; }, false, lifetime.signal);
|
|
23
|
+
if (code !== 0)
|
|
24
|
+
throw new Error('stage failed');
|
|
25
|
+
}
|
|
26
|
+
if (!stopped)
|
|
27
|
+
process.exitCode = (await executeJob(spec, 'run', undefined, c => { child = c; }, true, lifetime.signal)) ?? 1;
|
|
28
|
+
else
|
|
29
|
+
process.exitCode = 143;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
process.stderr.write('远端单次运行失败;需要连接诊断,根因未查明。\n');
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type RequestMessage } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { type AgentDefinition } from './agents.ts';
|
|
4
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
5
|
+
import type { WorkspaceNotice, SessionProjectTarget } from './connect-contract.ts';
|
|
6
|
+
/** X-owned adapter metadata; this is not a DSH renderer acknowledgement. */
|
|
7
|
+
export interface WorkspaceNoticeMarker {
|
|
8
|
+
readonly kind: 'workspace-notice';
|
|
9
|
+
readonly version: 1;
|
|
10
|
+
readonly provider: string;
|
|
11
|
+
readonly loginProvider: 'claude' | 'codex';
|
|
12
|
+
readonly sessionId: string;
|
|
13
|
+
readonly noticeId: string;
|
|
14
|
+
readonly claimId: string;
|
|
15
|
+
readonly deliveryId: string;
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
readonly workspaceName: string;
|
|
18
|
+
readonly bindingId: string;
|
|
19
|
+
readonly workspaceId: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseWorkspaceNoticeMarker(message: unknown): WorkspaceNoticeMarker | undefined;
|
|
22
|
+
/** Only this plugin's complete, source-bound notice may be excluded from remote model history. */
|
|
23
|
+
export declare function isWorkspaceNoticeMessage(message: unknown): boolean;
|
|
24
|
+
/** A verified first block in the actual model reply, never a second final message. */
|
|
25
|
+
export interface WorkspaceNoticePresentation {
|
|
26
|
+
readonly kind: 'workspace-notice';
|
|
27
|
+
readonly version: 2;
|
|
28
|
+
readonly provider: string;
|
|
29
|
+
readonly loginProvider: 'claude' | 'codex';
|
|
30
|
+
readonly sessionId: string;
|
|
31
|
+
readonly noticeId: string;
|
|
32
|
+
readonly claimId: string;
|
|
33
|
+
readonly workspaceName: string;
|
|
34
|
+
readonly bindingId: string;
|
|
35
|
+
readonly workspaceId: string;
|
|
36
|
+
readonly turn: number;
|
|
37
|
+
readonly step: number;
|
|
38
|
+
readonly blockIndex: 0;
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function workspaceNoticeFromMessage(message: unknown): WorkspaceNoticePresentation | undefined;
|
|
42
|
+
/** Preserve the model answer and exact remote replay pointer; ordinary text is never searched. */
|
|
43
|
+
export declare function withoutWorkspaceNotice<T extends RequestMessage>(message: T): T;
|
|
44
|
+
export declare function createWorkspaceNoticeConsumer(ctx: Context, agent: AgentDefinition): {
|
|
45
|
+
reconcile: (request: RuntimeRequest, api: ConnectApi) => Promise<void>;
|
|
46
|
+
deliver: (notice: WorkspaceNotice, resolved: SessionProjectTarget, request: RuntimeRequest, api: ConnectApi) => Promise<WorkspaceNoticePresentation | undefined>;
|
|
47
|
+
};
|