@junwu168/openshell 0.1.2 → 0.1.4
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/cli/openshell.js +4 -0
- package/dist/core/audit/log-store.js +1 -1
- package/dist/core/orchestrator.d.ts +2 -2
- package/dist/core/orchestrator.js +3 -3
- package/dist/core/result.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/opencode/plugin.d.ts +1 -1
- package/dist/opencode/plugin.js +8 -8
- package/package.json +6 -1
- package/.claude/settings.local.json +0 -15
- package/bun.lock +0 -368
- package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
- package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
- package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
- package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
- package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
- package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
- package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
- package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
- package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
- package/examples/opencode-local/opencode.json +0 -19
- package/scripts/openshell.ts +0 -3
- package/scripts/server-registry.ts +0 -3
- package/src/cli/openshell.ts +0 -60
- package/src/cli/server-registry.ts +0 -476
- package/src/core/audit/git-audit-repo.ts +0 -42
- package/src/core/audit/log-store.ts +0 -20
- package/src/core/audit/redact.ts +0 -4
- package/src/core/contracts.ts +0 -51
- package/src/core/orchestrator.ts +0 -1082
- package/src/core/patch.ts +0 -11
- package/src/core/paths.ts +0 -32
- package/src/core/policy.ts +0 -30
- package/src/core/registry/server-registry.ts +0 -505
- package/src/core/result.ts +0 -16
- package/src/core/ssh/ssh-runtime.ts +0 -355
- package/src/index.ts +0 -3
- package/src/opencode/plugin.ts +0 -242
- package/src/product/install.ts +0 -43
- package/src/product/opencode-config.ts +0 -118
- package/src/product/uninstall.ts +0 -47
- package/src/product/workspace-tracker.ts +0 -69
- package/tests/integration/fake-ssh-server.ts +0 -97
- package/tests/integration/install-lifecycle.test.ts +0 -85
- package/tests/integration/orchestrator.test.ts +0 -767
- package/tests/integration/ssh-runtime.test.ts +0 -122
- package/tests/unit/audit.test.ts +0 -221
- package/tests/unit/build-layout.test.ts +0 -28
- package/tests/unit/opencode-config.test.ts +0 -100
- package/tests/unit/opencode-plugin.test.ts +0 -358
- package/tests/unit/openshell-cli.test.ts +0 -60
- package/tests/unit/paths.test.ts +0 -64
- package/tests/unit/plugin-export.test.ts +0 -10
- package/tests/unit/policy.test.ts +0 -53
- package/tests/unit/release-docs.test.ts +0 -31
- package/tests/unit/result.test.ts +0 -28
- package/tests/unit/server-registry-cli.test.ts +0 -673
- package/tests/unit/server-registry.test.ts +0 -452
- package/tests/unit/workspace-tracker.test.ts +0 -57
- package/tsconfig.json +0 -14
package/src/core/orchestrator.ts
DELETED
|
@@ -1,1082 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs"
|
|
2
|
-
import { isAbsolute, resolve } from "node:path"
|
|
3
|
-
import type { ConnectConfig } from "ssh2"
|
|
4
|
-
import type { PolicyDecision, ToolPayload, ToolResult } from "./contracts"
|
|
5
|
-
import { applyUnifiedPatch } from "./patch"
|
|
6
|
-
import { classifyRemoteExec } from "./policy"
|
|
7
|
-
import { errorResult, okResult, partialFailureResult } from "./result"
|
|
8
|
-
import type { ResolvedServerRecord, ServerRecord, ServerRegistry } from "./registry/server-registry"
|
|
9
|
-
|
|
10
|
-
type ExecResult = {
|
|
11
|
-
stdout: string
|
|
12
|
-
stderr: string
|
|
13
|
-
exitCode: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type SshRuntime = {
|
|
17
|
-
exec(connection: ConnectConfig, command: string, options?: { cwd?: string; timeout?: number }): Promise<ExecResult>
|
|
18
|
-
readFile(connection: ConnectConfig, path: string): Promise<string>
|
|
19
|
-
writeFile(connection: ConnectConfig, path: string, content: string, mode?: number): Promise<void>
|
|
20
|
-
listDir(connection: ConnectConfig, path: string, recursive?: boolean, limit?: number): Promise<
|
|
21
|
-
{ name: string; longname: string }[] | string[]
|
|
22
|
-
>
|
|
23
|
-
stat(connection: ConnectConfig, path: string): Promise<{
|
|
24
|
-
size: number
|
|
25
|
-
mode: number
|
|
26
|
-
isFile: boolean
|
|
27
|
-
isDirectory: boolean
|
|
28
|
-
}>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type AuditEngine = {
|
|
32
|
-
preflightLog(): Promise<void>
|
|
33
|
-
appendLog(entry: Record<string, unknown>): Promise<void>
|
|
34
|
-
preflightSnapshots?(): Promise<void>
|
|
35
|
-
captureSnapshots?(input: { server: string; path: string; before: string; after: string }): Promise<void>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type PolicyEngine = {
|
|
39
|
-
classifyRemoteExec(command: string): {
|
|
40
|
-
decision: PolicyDecision
|
|
41
|
-
reason: string
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type OrchestratorOptions = {
|
|
46
|
-
registry: Pick<ServerRegistry, "list" | "resolve">
|
|
47
|
-
ssh: SshRuntime
|
|
48
|
-
audit: AuditEngine
|
|
49
|
-
policy?: PolicyEngine
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type RemoteExecInput = {
|
|
53
|
-
server: string
|
|
54
|
-
command: string
|
|
55
|
-
cwd?: string
|
|
56
|
-
timeout?: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type RemoteReadFileInput = {
|
|
60
|
-
server: string
|
|
61
|
-
path: string
|
|
62
|
-
offset?: number
|
|
63
|
-
length?: number
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type RemoteWriteFileInput = {
|
|
67
|
-
server: string
|
|
68
|
-
path: string
|
|
69
|
-
content: string
|
|
70
|
-
mode?: number
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type RemotePatchFileInput = {
|
|
74
|
-
server: string
|
|
75
|
-
path: string
|
|
76
|
-
patch: string
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type RemoteListDirInput = {
|
|
80
|
-
server: string
|
|
81
|
-
path: string
|
|
82
|
-
recursive?: boolean
|
|
83
|
-
limit?: number
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type RemoteStatInput = {
|
|
87
|
-
server: string
|
|
88
|
-
path: string
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type RemoteFindInput = {
|
|
92
|
-
server: string
|
|
93
|
-
path: string
|
|
94
|
-
pattern: string
|
|
95
|
-
glob?: string
|
|
96
|
-
limit?: number
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const quoteShell = (value: string) => `'${value.replaceAll("'", `'\"'\"'`)}'`
|
|
100
|
-
|
|
101
|
-
type AuthPathErrorCode = "AUTH_PATH_INVALID" | "KEY_PATH_NOT_FOUND" | "CERTIFICATE_PATH_NOT_FOUND" | "AUTH_PATH_UNREADABLE"
|
|
102
|
-
|
|
103
|
-
const authError = <T>(
|
|
104
|
-
tool: string,
|
|
105
|
-
server: ResolvedServerRecord,
|
|
106
|
-
code: AuthPathErrorCode,
|
|
107
|
-
message: string,
|
|
108
|
-
): ToolResult<T> =>
|
|
109
|
-
errorResult({
|
|
110
|
-
tool,
|
|
111
|
-
server: server.id,
|
|
112
|
-
code,
|
|
113
|
-
message,
|
|
114
|
-
execution: { attempted: false, completed: false },
|
|
115
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const resolveAuthPath = (
|
|
119
|
-
server: ResolvedServerRecord,
|
|
120
|
-
pathValue: string,
|
|
121
|
-
): { path: string } | { code: AuthPathErrorCode; message: string } => {
|
|
122
|
-
if (isAbsolute(pathValue)) {
|
|
123
|
-
return { path: pathValue }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (server.scope !== "workspace" || !server.workspaceRoot) {
|
|
127
|
-
return {
|
|
128
|
-
code: "AUTH_PATH_INVALID",
|
|
129
|
-
message: `Relative auth paths are only allowed for workspace-scoped records: ${pathValue}`,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
path: resolve(server.workspaceRoot, pathValue),
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const readAuthFile = (
|
|
139
|
-
server: ResolvedServerRecord,
|
|
140
|
-
tool: string,
|
|
141
|
-
pathValue: string,
|
|
142
|
-
missingCode: "KEY_PATH_NOT_FOUND" | "CERTIFICATE_PATH_NOT_FOUND",
|
|
143
|
-
): { content: string } | ToolResult<never> => {
|
|
144
|
-
const resolved = resolveAuthPath(server, pathValue)
|
|
145
|
-
if ("code" in resolved) {
|
|
146
|
-
return authError(tool, server, resolved.code, resolved.message)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
return { content: readFileSync(resolved.path, "utf8") }
|
|
151
|
-
} catch (error) {
|
|
152
|
-
const errno = (error as NodeJS.ErrnoException).code
|
|
153
|
-
if (errno === "ENOENT") {
|
|
154
|
-
return authError(tool, server, missingCode, `Auth file not found: ${resolved.path}`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return authError(tool, server, "AUTH_PATH_UNREADABLE", `Auth file is unreadable: ${resolved.path}`)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const toConnectConfig = (tool: string, server: ResolvedServerRecord): ConnectConfig | ToolResult<never> => {
|
|
162
|
-
const base = {
|
|
163
|
-
host: server.host,
|
|
164
|
-
port: server.port,
|
|
165
|
-
username: server.username,
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
switch (server.auth.kind) {
|
|
169
|
-
case "password":
|
|
170
|
-
return {
|
|
171
|
-
...base,
|
|
172
|
-
password: server.auth.secret,
|
|
173
|
-
}
|
|
174
|
-
case "privateKey": {
|
|
175
|
-
const privateKey = readAuthFile(server, tool, server.auth.privateKeyPath, "KEY_PATH_NOT_FOUND")
|
|
176
|
-
if ("status" in privateKey) {
|
|
177
|
-
return privateKey
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
...base,
|
|
182
|
-
privateKey: privateKey.content,
|
|
183
|
-
...(server.auth.passphrase ? { passphrase: server.auth.passphrase } : {}),
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
case "certificate": {
|
|
187
|
-
const certificate = readAuthFile(server, tool, server.auth.certificatePath, "CERTIFICATE_PATH_NOT_FOUND")
|
|
188
|
-
if ("status" in certificate) {
|
|
189
|
-
return certificate
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const privateKey = readAuthFile(server, tool, server.auth.privateKeyPath, "KEY_PATH_NOT_FOUND")
|
|
193
|
-
if ("status" in privateKey) {
|
|
194
|
-
return privateKey
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
...base,
|
|
199
|
-
privateKey: privateKey.content,
|
|
200
|
-
...(server.auth.passphrase ? { passphrase: server.auth.passphrase } : {}),
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const withAuditFlag = <T>(
|
|
207
|
-
status: "ok" | "partial_failure" | "error",
|
|
208
|
-
payload: ToolPayload<T>,
|
|
209
|
-
logWritten: boolean,
|
|
210
|
-
): ToolResult<T> => {
|
|
211
|
-
const next: ToolPayload<T> = {
|
|
212
|
-
...payload,
|
|
213
|
-
audit: {
|
|
214
|
-
logWritten,
|
|
215
|
-
snapshotStatus: payload.audit?.snapshotStatus ?? "not-applicable",
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (status === "ok") {
|
|
220
|
-
return okResult(next)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (status === "partial_failure") {
|
|
224
|
-
return partialFailureResult(next)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return errorResult(next)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const byteLength = (value: string) => Buffer.byteLength(value)
|
|
231
|
-
const clampLimit = (value: number | undefined, fallback: number) => Math.max(1, Math.trunc(value ?? fallback))
|
|
232
|
-
|
|
233
|
-
export const createOrchestrator = ({ registry, ssh, audit, policy = { classifyRemoteExec } }: OrchestratorOptions) => {
|
|
234
|
-
const appendLogSafe = async (entry: Record<string, unknown>) => {
|
|
235
|
-
try {
|
|
236
|
-
await audit.appendLog(entry)
|
|
237
|
-
return true
|
|
238
|
-
} catch {
|
|
239
|
-
return false
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const isRegistryValidationError = (
|
|
244
|
-
error: unknown,
|
|
245
|
-
): error is { code: "REGISTRY_RECORD_INVALID"; message: string } =>
|
|
246
|
-
typeof error === "object" &&
|
|
247
|
-
error !== null &&
|
|
248
|
-
(error as { code?: unknown }).code === "REGISTRY_RECORD_INVALID" &&
|
|
249
|
-
typeof (error as { message?: unknown }).message === "string"
|
|
250
|
-
|
|
251
|
-
const preflightLog = async <T>(tool: string, server?: string): Promise<ToolResult<T> | null> => {
|
|
252
|
-
try {
|
|
253
|
-
await audit.preflightLog()
|
|
254
|
-
return null
|
|
255
|
-
} catch (error) {
|
|
256
|
-
return errorResult({
|
|
257
|
-
tool,
|
|
258
|
-
server,
|
|
259
|
-
code: "AUDIT_LOG_PREFLIGHT_FAILED",
|
|
260
|
-
message: (error as Error).message,
|
|
261
|
-
execution: { attempted: false, completed: false },
|
|
262
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const logAuthFailure = async <T>(
|
|
268
|
-
tool: string,
|
|
269
|
-
approvalStatus: string,
|
|
270
|
-
logEntry: Record<string, unknown>,
|
|
271
|
-
result: ToolResult<T>,
|
|
272
|
-
): Promise<ToolResult<T>> => {
|
|
273
|
-
const logWritten = await appendLogSafe({
|
|
274
|
-
...logEntry,
|
|
275
|
-
tool,
|
|
276
|
-
server: result.server,
|
|
277
|
-
approvalStatus,
|
|
278
|
-
code: result.code,
|
|
279
|
-
message: result.message,
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
return withAuditFlag("error", result, logWritten)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const resolveServer = async <T>(
|
|
286
|
-
tool: string,
|
|
287
|
-
serverId: string,
|
|
288
|
-
logEntry: Record<string, unknown>,
|
|
289
|
-
approvalStatus: string,
|
|
290
|
-
): Promise<{ result: ToolResult<T> | null; server: ResolvedServerRecord | null }> => {
|
|
291
|
-
let server: ResolvedServerRecord | null
|
|
292
|
-
try {
|
|
293
|
-
server = await registry.resolve(serverId)
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (isRegistryValidationError(error)) {
|
|
296
|
-
const payload: ToolPayload<T> = {
|
|
297
|
-
tool,
|
|
298
|
-
server: serverId,
|
|
299
|
-
code: error.code,
|
|
300
|
-
message: error.message,
|
|
301
|
-
execution: { attempted: false, completed: false },
|
|
302
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const logWritten = await appendLogSafe({
|
|
306
|
-
...logEntry,
|
|
307
|
-
tool,
|
|
308
|
-
server: serverId,
|
|
309
|
-
approvalStatus,
|
|
310
|
-
code: error.code,
|
|
311
|
-
message: error.message,
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
result: withAuditFlag("error", payload, logWritten),
|
|
316
|
-
server: null,
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const payload: ToolPayload<T> = {
|
|
321
|
-
tool,
|
|
322
|
-
server: serverId,
|
|
323
|
-
code: "SERVER_RESOLVE_FAILED",
|
|
324
|
-
message: (error as Error).message,
|
|
325
|
-
execution: { attempted: false, completed: false },
|
|
326
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const logWritten = await appendLogSafe({
|
|
330
|
-
...logEntry,
|
|
331
|
-
tool,
|
|
332
|
-
server: serverId,
|
|
333
|
-
approvalStatus,
|
|
334
|
-
code: "SERVER_RESOLVE_FAILED",
|
|
335
|
-
message: payload.message,
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
result: withAuditFlag("error", payload, logWritten),
|
|
340
|
-
server: null,
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (!server) {
|
|
345
|
-
const payload: ToolPayload<T> = {
|
|
346
|
-
tool,
|
|
347
|
-
server: serverId,
|
|
348
|
-
code: "SERVER_NOT_FOUND",
|
|
349
|
-
execution: { attempted: false, completed: false },
|
|
350
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const logWritten = await appendLogSafe({
|
|
354
|
-
...logEntry,
|
|
355
|
-
tool,
|
|
356
|
-
server: serverId,
|
|
357
|
-
approvalStatus,
|
|
358
|
-
code: "SERVER_NOT_FOUND",
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
result: withAuditFlag("error", payload, logWritten),
|
|
363
|
-
server: null,
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
result: null,
|
|
369
|
-
server,
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const listServers = async (): Promise<ToolResult<Array<Omit<ServerRecord, "auth">>>> => {
|
|
374
|
-
const logReady = await preflightLog<Array<Omit<ServerRecord, "auth">>>("list_servers")
|
|
375
|
-
if (logReady) {
|
|
376
|
-
return logReady
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
const servers = await registry.list()
|
|
381
|
-
const data = servers.map(({ auth: _auth, workspaceRoot: _workspaceRoot, ...server }) => server)
|
|
382
|
-
const payload: ToolPayload<Array<Omit<ServerRecord, "auth">>> = {
|
|
383
|
-
tool: "list_servers",
|
|
384
|
-
data,
|
|
385
|
-
execution: { attempted: true, completed: true },
|
|
386
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
387
|
-
}
|
|
388
|
-
const logWritten = await appendLogSafe({
|
|
389
|
-
tool: "list_servers",
|
|
390
|
-
approvalStatus: "not-required",
|
|
391
|
-
count: data.length,
|
|
392
|
-
})
|
|
393
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (isRegistryValidationError(error)) {
|
|
396
|
-
const payload: ToolPayload<Array<Omit<ServerRecord, "auth">>> = {
|
|
397
|
-
tool: "list_servers",
|
|
398
|
-
code: error.code,
|
|
399
|
-
message: error.message,
|
|
400
|
-
execution: { attempted: false, completed: false },
|
|
401
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
402
|
-
}
|
|
403
|
-
const logWritten = await appendLogSafe({
|
|
404
|
-
tool: "list_servers",
|
|
405
|
-
approvalStatus: "not-required",
|
|
406
|
-
code: error.code,
|
|
407
|
-
message: payload.message,
|
|
408
|
-
})
|
|
409
|
-
return withAuditFlag("error", payload, logWritten)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const payload: ToolPayload<Array<Omit<ServerRecord, "auth">>> = {
|
|
413
|
-
tool: "list_servers",
|
|
414
|
-
code: "REGISTRY_LIST_FAILED",
|
|
415
|
-
message: (error as Error).message,
|
|
416
|
-
execution: { attempted: false, completed: false },
|
|
417
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
418
|
-
}
|
|
419
|
-
const logWritten = await appendLogSafe({
|
|
420
|
-
tool: "list_servers",
|
|
421
|
-
approvalStatus: "not-required",
|
|
422
|
-
code: "REGISTRY_LIST_FAILED",
|
|
423
|
-
message: payload.message,
|
|
424
|
-
})
|
|
425
|
-
return withAuditFlag("error", payload, logWritten)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const remoteExec = async (input: RemoteExecInput): Promise<ToolResult<ExecResult>> => {
|
|
430
|
-
const logReady = await preflightLog<ExecResult>("remote_exec", input.server)
|
|
431
|
-
if (logReady) {
|
|
432
|
-
return logReady
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const classification = policy.classifyRemoteExec(input.command)
|
|
436
|
-
const approvalStatus =
|
|
437
|
-
classification.decision === "approval-required" ? "host-managed-required" : "not-required"
|
|
438
|
-
|
|
439
|
-
const resolved = await resolveServer(
|
|
440
|
-
"remote_exec",
|
|
441
|
-
input.server,
|
|
442
|
-
{ command: input.command, cwd: input.cwd, timeout: input.timeout },
|
|
443
|
-
"unknown",
|
|
444
|
-
)
|
|
445
|
-
if (resolved.result) {
|
|
446
|
-
return resolved.result as ToolResult<ExecResult>
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (classification.decision === "reject") {
|
|
450
|
-
const payload: ToolPayload<ExecResult> = {
|
|
451
|
-
tool: "remote_exec",
|
|
452
|
-
server: input.server,
|
|
453
|
-
code: "POLICY_REJECTED",
|
|
454
|
-
message: classification.reason,
|
|
455
|
-
execution: { attempted: false, completed: false },
|
|
456
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
457
|
-
}
|
|
458
|
-
const logWritten = await appendLogSafe({
|
|
459
|
-
tool: "remote_exec",
|
|
460
|
-
server: input.server,
|
|
461
|
-
command: input.command,
|
|
462
|
-
cwd: input.cwd,
|
|
463
|
-
timeout: input.timeout,
|
|
464
|
-
approvalStatus: "not-required",
|
|
465
|
-
code: "POLICY_REJECTED",
|
|
466
|
-
message: classification.reason,
|
|
467
|
-
})
|
|
468
|
-
return withAuditFlag("error", payload, logWritten)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
const connection = toConnectConfig("remote_exec", resolved.server!)
|
|
473
|
-
if ("status" in connection) {
|
|
474
|
-
return logAuthFailure(
|
|
475
|
-
"remote_exec",
|
|
476
|
-
approvalStatus,
|
|
477
|
-
{ command: input.command, cwd: input.cwd, timeout: input.timeout },
|
|
478
|
-
connection,
|
|
479
|
-
)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const executed = await ssh.exec(connection, input.command, {
|
|
483
|
-
cwd: input.cwd,
|
|
484
|
-
timeout: input.timeout,
|
|
485
|
-
})
|
|
486
|
-
const payload: ToolPayload<ExecResult> = {
|
|
487
|
-
tool: "remote_exec",
|
|
488
|
-
server: input.server,
|
|
489
|
-
data: executed,
|
|
490
|
-
execution: {
|
|
491
|
-
attempted: true,
|
|
492
|
-
completed: true,
|
|
493
|
-
exitCode: executed.exitCode,
|
|
494
|
-
stdoutBytes: byteLength(executed.stdout),
|
|
495
|
-
stderrBytes: byteLength(executed.stderr),
|
|
496
|
-
},
|
|
497
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
498
|
-
}
|
|
499
|
-
const logWritten = await appendLogSafe({
|
|
500
|
-
tool: "remote_exec",
|
|
501
|
-
server: input.server,
|
|
502
|
-
command: input.command,
|
|
503
|
-
cwd: input.cwd,
|
|
504
|
-
timeout: input.timeout,
|
|
505
|
-
approvalStatus,
|
|
506
|
-
policyDecision: classification.decision,
|
|
507
|
-
approvalRequired: classification.decision === "approval-required",
|
|
508
|
-
...executed,
|
|
509
|
-
})
|
|
510
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
511
|
-
} catch (error) {
|
|
512
|
-
const payload: ToolPayload<ExecResult> = {
|
|
513
|
-
tool: "remote_exec",
|
|
514
|
-
server: input.server,
|
|
515
|
-
code: "SSH_EXEC_FAILED",
|
|
516
|
-
message: (error as Error).message,
|
|
517
|
-
execution: { attempted: true, completed: false },
|
|
518
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
519
|
-
}
|
|
520
|
-
const logWritten = await appendLogSafe({
|
|
521
|
-
tool: "remote_exec",
|
|
522
|
-
server: input.server,
|
|
523
|
-
command: input.command,
|
|
524
|
-
cwd: input.cwd,
|
|
525
|
-
timeout: input.timeout,
|
|
526
|
-
approvalStatus,
|
|
527
|
-
code: "SSH_EXEC_FAILED",
|
|
528
|
-
message: payload.message,
|
|
529
|
-
})
|
|
530
|
-
return withAuditFlag("error", payload, logWritten)
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const remoteReadFile = async (input: RemoteReadFileInput): Promise<ToolResult<{ content: string }>> => {
|
|
535
|
-
const logReady = await preflightLog<{ content: string }>("remote_read_file", input.server)
|
|
536
|
-
if (logReady) {
|
|
537
|
-
return logReady
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const resolved = await resolveServer(
|
|
541
|
-
"remote_read_file",
|
|
542
|
-
input.server,
|
|
543
|
-
{ path: input.path, offset: input.offset, length: input.length },
|
|
544
|
-
"not-required",
|
|
545
|
-
)
|
|
546
|
-
if (resolved.result) {
|
|
547
|
-
return resolved.result as ToolResult<{ content: string }>
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
const connection = toConnectConfig("remote_read_file", resolved.server!)
|
|
552
|
-
if ("status" in connection) {
|
|
553
|
-
return logAuthFailure(
|
|
554
|
-
"remote_read_file",
|
|
555
|
-
"not-required",
|
|
556
|
-
{ path: input.path, offset: input.offset, length: input.length },
|
|
557
|
-
connection,
|
|
558
|
-
)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const body = await ssh.readFile(connection, input.path)
|
|
562
|
-
const offset = input.offset ?? 0
|
|
563
|
-
const content = body.slice(offset, input.length ? offset + input.length : undefined)
|
|
564
|
-
const payload: ToolPayload<{ content: string }> = {
|
|
565
|
-
tool: "remote_read_file",
|
|
566
|
-
server: input.server,
|
|
567
|
-
data: { content },
|
|
568
|
-
execution: { attempted: true, completed: true },
|
|
569
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
570
|
-
}
|
|
571
|
-
const logWritten = await appendLogSafe({
|
|
572
|
-
tool: "remote_read_file",
|
|
573
|
-
server: input.server,
|
|
574
|
-
path: input.path,
|
|
575
|
-
offset: input.offset,
|
|
576
|
-
length: input.length,
|
|
577
|
-
approvalStatus: "not-required",
|
|
578
|
-
})
|
|
579
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
580
|
-
} catch (error) {
|
|
581
|
-
const payload: ToolPayload<{ content: string }> = {
|
|
582
|
-
tool: "remote_read_file",
|
|
583
|
-
server: input.server,
|
|
584
|
-
code: "SSH_READ_FAILED",
|
|
585
|
-
message: (error as Error).message,
|
|
586
|
-
execution: { attempted: true, completed: false },
|
|
587
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
588
|
-
}
|
|
589
|
-
const logWritten = await appendLogSafe({
|
|
590
|
-
tool: "remote_read_file",
|
|
591
|
-
server: input.server,
|
|
592
|
-
path: input.path,
|
|
593
|
-
approvalStatus: "not-required",
|
|
594
|
-
code: "SSH_READ_FAILED",
|
|
595
|
-
message: payload.message,
|
|
596
|
-
})
|
|
597
|
-
return withAuditFlag("error", payload, logWritten)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const remoteWriteFile = async (input: RemoteWriteFileInput): Promise<ToolResult> => {
|
|
602
|
-
const logReady = await preflightLog("remote_write_file", input.server)
|
|
603
|
-
if (logReady) {
|
|
604
|
-
return logReady
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const resolved = await resolveServer(
|
|
608
|
-
"remote_write_file",
|
|
609
|
-
input.server,
|
|
610
|
-
{ path: input.path, mode: input.mode },
|
|
611
|
-
"host-managed-required",
|
|
612
|
-
)
|
|
613
|
-
if (resolved.result) {
|
|
614
|
-
return resolved.result
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const connection = toConnectConfig("remote_write_file", resolved.server!)
|
|
618
|
-
if ("status" in connection) {
|
|
619
|
-
return logAuthFailure("remote_write_file", "host-managed-required", { path: input.path, mode: input.mode }, connection)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
await (audit.preflightSnapshots?.() ?? Promise.resolve())
|
|
624
|
-
} catch (error) {
|
|
625
|
-
const payload: ToolPayload<ExecResult> = {
|
|
626
|
-
tool: "remote_write_file",
|
|
627
|
-
server: input.server,
|
|
628
|
-
code: "AUDIT_SNAPSHOT_PREFLIGHT_FAILED",
|
|
629
|
-
message: (error as Error).message,
|
|
630
|
-
execution: { attempted: false, completed: false },
|
|
631
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
632
|
-
}
|
|
633
|
-
const logWritten = await appendLogSafe({
|
|
634
|
-
tool: "remote_write_file",
|
|
635
|
-
server: input.server,
|
|
636
|
-
path: input.path,
|
|
637
|
-
mode: input.mode,
|
|
638
|
-
approvalStatus: "host-managed-required",
|
|
639
|
-
code: "AUDIT_SNAPSHOT_PREFLIGHT_FAILED",
|
|
640
|
-
message: payload.message,
|
|
641
|
-
})
|
|
642
|
-
return withAuditFlag("error", payload, logWritten)
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const before = await ssh.readFile(connection, input.path).catch(() => "")
|
|
646
|
-
try {
|
|
647
|
-
await ssh.writeFile(connection, input.path, input.content, input.mode)
|
|
648
|
-
} catch (error) {
|
|
649
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
650
|
-
tool: "remote_write_file",
|
|
651
|
-
server: input.server,
|
|
652
|
-
code: "SSH_WRITE_FAILED",
|
|
653
|
-
message: (error as Error).message,
|
|
654
|
-
execution: { attempted: true, completed: false },
|
|
655
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
656
|
-
}
|
|
657
|
-
const logWritten = await appendLogSafe({
|
|
658
|
-
tool: "remote_write_file",
|
|
659
|
-
server: input.server,
|
|
660
|
-
path: input.path,
|
|
661
|
-
mode: input.mode,
|
|
662
|
-
approvalStatus: "host-managed-required",
|
|
663
|
-
approvalRequired: true,
|
|
664
|
-
code: "SSH_WRITE_FAILED",
|
|
665
|
-
message: payload.message,
|
|
666
|
-
})
|
|
667
|
-
return withAuditFlag("error", payload, logWritten)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const after = await ssh.readFile(connection, input.path).catch(() => input.content)
|
|
671
|
-
const logWritten = await appendLogSafe({
|
|
672
|
-
tool: "remote_write_file",
|
|
673
|
-
server: input.server,
|
|
674
|
-
path: input.path,
|
|
675
|
-
mode: input.mode,
|
|
676
|
-
changedPath: input.path,
|
|
677
|
-
approvalStatus: "host-managed-required",
|
|
678
|
-
approvalRequired: true,
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
try {
|
|
682
|
-
await (audit.captureSnapshots?.({ server: input.server, path: input.path, before, after }) ?? Promise.resolve())
|
|
683
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
684
|
-
tool: "remote_write_file",
|
|
685
|
-
server: input.server,
|
|
686
|
-
execution: { attempted: true, completed: true },
|
|
687
|
-
audit: { logWritten: false, snapshotStatus: "written" },
|
|
688
|
-
}
|
|
689
|
-
if (!logWritten) {
|
|
690
|
-
return withAuditFlag(
|
|
691
|
-
"partial_failure",
|
|
692
|
-
{
|
|
693
|
-
...payload,
|
|
694
|
-
message: "remote write succeeded but audit log write failed",
|
|
695
|
-
audit: { logWritten: false, snapshotStatus: "written" },
|
|
696
|
-
},
|
|
697
|
-
false,
|
|
698
|
-
)
|
|
699
|
-
}
|
|
700
|
-
return withAuditFlag("ok", payload, true)
|
|
701
|
-
} catch (error) {
|
|
702
|
-
return withAuditFlag(
|
|
703
|
-
"partial_failure",
|
|
704
|
-
{
|
|
705
|
-
tool: "remote_write_file",
|
|
706
|
-
server: input.server,
|
|
707
|
-
message: `remote write succeeded but audit finalization failed: ${(error as Error).message}`,
|
|
708
|
-
execution: { attempted: true, completed: true },
|
|
709
|
-
audit: { logWritten: false, snapshotStatus: "partial-failure" },
|
|
710
|
-
},
|
|
711
|
-
logWritten,
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const remotePatchFile = async (input: RemotePatchFileInput): Promise<ToolResult> => {
|
|
717
|
-
const logReady = await preflightLog("remote_patch_file", input.server)
|
|
718
|
-
if (logReady) {
|
|
719
|
-
return logReady
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const resolved = await resolveServer(
|
|
723
|
-
"remote_patch_file",
|
|
724
|
-
input.server,
|
|
725
|
-
{ path: input.path },
|
|
726
|
-
"host-managed-required",
|
|
727
|
-
)
|
|
728
|
-
if (resolved.result) {
|
|
729
|
-
return resolved.result
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const connection = toConnectConfig("remote_patch_file", resolved.server!)
|
|
733
|
-
if ("status" in connection) {
|
|
734
|
-
return logAuthFailure("remote_patch_file", "host-managed-required", { path: input.path }, connection)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
let before: string
|
|
738
|
-
try {
|
|
739
|
-
before = await ssh.readFile(connection, input.path)
|
|
740
|
-
} catch (error) {
|
|
741
|
-
const payload: ToolPayload<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }> = {
|
|
742
|
-
tool: "remote_patch_file",
|
|
743
|
-
server: input.server,
|
|
744
|
-
code: "SSH_READ_FAILED",
|
|
745
|
-
message: (error as Error).message,
|
|
746
|
-
execution: { attempted: true, completed: false },
|
|
747
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
748
|
-
}
|
|
749
|
-
const logWritten = await appendLogSafe({
|
|
750
|
-
tool: "remote_patch_file",
|
|
751
|
-
server: input.server,
|
|
752
|
-
path: input.path,
|
|
753
|
-
approvalStatus: "host-managed-required",
|
|
754
|
-
code: "SSH_READ_FAILED",
|
|
755
|
-
message: payload.message,
|
|
756
|
-
})
|
|
757
|
-
return withAuditFlag("error", payload, logWritten)
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
let after: string
|
|
761
|
-
try {
|
|
762
|
-
after = applyUnifiedPatch(before, input.patch)
|
|
763
|
-
} catch (error) {
|
|
764
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
765
|
-
tool: "remote_patch_file",
|
|
766
|
-
server: input.server,
|
|
767
|
-
code: "PATCH_APPLY_FAILED",
|
|
768
|
-
message: (error as Error).message,
|
|
769
|
-
execution: { attempted: false, completed: false },
|
|
770
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
771
|
-
}
|
|
772
|
-
const logWritten = await appendLogSafe({
|
|
773
|
-
tool: "remote_patch_file",
|
|
774
|
-
server: input.server,
|
|
775
|
-
path: input.path,
|
|
776
|
-
approvalStatus: "host-managed-required",
|
|
777
|
-
code: "PATCH_APPLY_FAILED",
|
|
778
|
-
message: payload.message,
|
|
779
|
-
})
|
|
780
|
-
return withAuditFlag("error", payload, logWritten)
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
try {
|
|
784
|
-
await (audit.preflightSnapshots?.() ?? Promise.resolve())
|
|
785
|
-
} catch (error) {
|
|
786
|
-
const payload: ToolPayload<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }> = {
|
|
787
|
-
tool: "remote_patch_file",
|
|
788
|
-
server: input.server,
|
|
789
|
-
code: "AUDIT_SNAPSHOT_PREFLIGHT_FAILED",
|
|
790
|
-
message: (error as Error).message,
|
|
791
|
-
execution: { attempted: false, completed: false },
|
|
792
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
793
|
-
}
|
|
794
|
-
const logWritten = await appendLogSafe({
|
|
795
|
-
tool: "remote_patch_file",
|
|
796
|
-
server: input.server,
|
|
797
|
-
path: input.path,
|
|
798
|
-
approvalStatus: "host-managed-required",
|
|
799
|
-
code: "AUDIT_SNAPSHOT_PREFLIGHT_FAILED",
|
|
800
|
-
message: payload.message,
|
|
801
|
-
})
|
|
802
|
-
return withAuditFlag("error", payload, logWritten)
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
await ssh.writeFile(connection, input.path, after)
|
|
807
|
-
} catch (error) {
|
|
808
|
-
const payload: ToolPayload<ExecResult> = {
|
|
809
|
-
tool: "remote_patch_file",
|
|
810
|
-
server: input.server,
|
|
811
|
-
code: "SSH_WRITE_FAILED",
|
|
812
|
-
message: (error as Error).message,
|
|
813
|
-
execution: { attempted: true, completed: false },
|
|
814
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
815
|
-
}
|
|
816
|
-
const logWritten = await appendLogSafe({
|
|
817
|
-
tool: "remote_patch_file",
|
|
818
|
-
server: input.server,
|
|
819
|
-
path: input.path,
|
|
820
|
-
approvalStatus: "host-managed-required",
|
|
821
|
-
approvalRequired: true,
|
|
822
|
-
code: "SSH_WRITE_FAILED",
|
|
823
|
-
message: payload.message,
|
|
824
|
-
})
|
|
825
|
-
return withAuditFlag("error", payload, logWritten)
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const logWritten = await appendLogSafe({
|
|
829
|
-
tool: "remote_patch_file",
|
|
830
|
-
server: input.server,
|
|
831
|
-
path: input.path,
|
|
832
|
-
changedPath: input.path,
|
|
833
|
-
approvalStatus: "host-managed-required",
|
|
834
|
-
approvalRequired: true,
|
|
835
|
-
})
|
|
836
|
-
|
|
837
|
-
try {
|
|
838
|
-
await (audit.captureSnapshots?.({ server: input.server, path: input.path, before, after }) ?? Promise.resolve())
|
|
839
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
840
|
-
tool: "remote_patch_file",
|
|
841
|
-
server: input.server,
|
|
842
|
-
execution: { attempted: true, completed: true },
|
|
843
|
-
audit: { logWritten: false, snapshotStatus: "written" },
|
|
844
|
-
}
|
|
845
|
-
if (!logWritten) {
|
|
846
|
-
return withAuditFlag(
|
|
847
|
-
"partial_failure",
|
|
848
|
-
{
|
|
849
|
-
...payload,
|
|
850
|
-
message: "remote patch succeeded but audit log write failed",
|
|
851
|
-
audit: { logWritten: false, snapshotStatus: "written" },
|
|
852
|
-
},
|
|
853
|
-
false,
|
|
854
|
-
)
|
|
855
|
-
}
|
|
856
|
-
return withAuditFlag("ok", payload, true)
|
|
857
|
-
} catch (error) {
|
|
858
|
-
return withAuditFlag(
|
|
859
|
-
"partial_failure",
|
|
860
|
-
{
|
|
861
|
-
tool: "remote_patch_file",
|
|
862
|
-
server: input.server,
|
|
863
|
-
message: `remote patch succeeded but audit finalization failed: ${(error as Error).message}`,
|
|
864
|
-
execution: { attempted: true, completed: true },
|
|
865
|
-
audit: { logWritten: false, snapshotStatus: "partial-failure" },
|
|
866
|
-
},
|
|
867
|
-
logWritten,
|
|
868
|
-
)
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const remoteListDir = async (
|
|
873
|
-
input: RemoteListDirInput,
|
|
874
|
-
): Promise<ToolResult<{ name: string; longname: string }[] | string[]>> => {
|
|
875
|
-
const logReady = await preflightLog<{ name: string; longname: string }[] | string[]>("remote_list_dir", input.server)
|
|
876
|
-
if (logReady) {
|
|
877
|
-
return logReady
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
const resolved = await resolveServer(
|
|
881
|
-
"remote_list_dir",
|
|
882
|
-
input.server,
|
|
883
|
-
{ path: input.path, recursive: input.recursive, limit: input.limit },
|
|
884
|
-
"not-required",
|
|
885
|
-
)
|
|
886
|
-
if (resolved.result) {
|
|
887
|
-
return resolved.result as ToolResult<{ name: string; longname: string }[] | string[]>
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
try {
|
|
891
|
-
const connection = toConnectConfig("remote_list_dir", resolved.server!)
|
|
892
|
-
if ("status" in connection) {
|
|
893
|
-
return logAuthFailure(
|
|
894
|
-
"remote_list_dir",
|
|
895
|
-
"not-required",
|
|
896
|
-
{ path: input.path, recursive: input.recursive ?? false, limit: input.limit ?? 200 },
|
|
897
|
-
connection,
|
|
898
|
-
)
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const entries = await ssh.listDir(connection, input.path, input.recursive ?? false, input.limit ?? 200)
|
|
902
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
903
|
-
tool: "remote_list_dir",
|
|
904
|
-
server: input.server,
|
|
905
|
-
data: entries,
|
|
906
|
-
execution: { attempted: true, completed: true },
|
|
907
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
908
|
-
}
|
|
909
|
-
const logWritten = await appendLogSafe({
|
|
910
|
-
tool: "remote_list_dir",
|
|
911
|
-
server: input.server,
|
|
912
|
-
path: input.path,
|
|
913
|
-
recursive: input.recursive ?? false,
|
|
914
|
-
limit: input.limit ?? 200,
|
|
915
|
-
approvalStatus: "not-required",
|
|
916
|
-
})
|
|
917
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
918
|
-
} catch (error) {
|
|
919
|
-
const payload: ToolPayload<{ name: string; longname: string }[] | string[]> = {
|
|
920
|
-
tool: "remote_list_dir",
|
|
921
|
-
server: input.server,
|
|
922
|
-
code: "SSH_LIST_FAILED",
|
|
923
|
-
message: (error as Error).message,
|
|
924
|
-
execution: { attempted: true, completed: false },
|
|
925
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
926
|
-
}
|
|
927
|
-
const logWritten = await appendLogSafe({
|
|
928
|
-
tool: "remote_list_dir",
|
|
929
|
-
server: input.server,
|
|
930
|
-
path: input.path,
|
|
931
|
-
approvalStatus: "not-required",
|
|
932
|
-
code: "SSH_LIST_FAILED",
|
|
933
|
-
message: payload.message,
|
|
934
|
-
})
|
|
935
|
-
return withAuditFlag("error", payload, logWritten)
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
const remoteStat = async (
|
|
940
|
-
input: RemoteStatInput,
|
|
941
|
-
): Promise<ToolResult<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }>> => {
|
|
942
|
-
const logReady = await preflightLog<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }>(
|
|
943
|
-
"remote_stat",
|
|
944
|
-
input.server,
|
|
945
|
-
)
|
|
946
|
-
if (logReady) {
|
|
947
|
-
return logReady
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const resolved = await resolveServer("remote_stat", input.server, { path: input.path }, "not-required")
|
|
951
|
-
if (resolved.result) {
|
|
952
|
-
return resolved.result as ToolResult<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }>
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
try {
|
|
956
|
-
const connection = toConnectConfig("remote_stat", resolved.server!)
|
|
957
|
-
if ("status" in connection) {
|
|
958
|
-
return logAuthFailure("remote_stat", "not-required", { path: input.path }, connection)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const stat = await ssh.stat(connection, input.path)
|
|
962
|
-
const payload: ToolPayload<typeof stat> = {
|
|
963
|
-
tool: "remote_stat",
|
|
964
|
-
server: input.server,
|
|
965
|
-
data: stat,
|
|
966
|
-
execution: { attempted: true, completed: true },
|
|
967
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
968
|
-
}
|
|
969
|
-
const logWritten = await appendLogSafe({
|
|
970
|
-
tool: "remote_stat",
|
|
971
|
-
server: input.server,
|
|
972
|
-
path: input.path,
|
|
973
|
-
approvalStatus: "not-required",
|
|
974
|
-
})
|
|
975
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
976
|
-
} catch (error) {
|
|
977
|
-
const payload: ToolPayload<{ size: number; mode: number; isFile: boolean; isDirectory: boolean }> = {
|
|
978
|
-
tool: "remote_stat",
|
|
979
|
-
server: input.server,
|
|
980
|
-
code: "SSH_STAT_FAILED",
|
|
981
|
-
message: (error as Error).message,
|
|
982
|
-
execution: { attempted: true, completed: false },
|
|
983
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
984
|
-
}
|
|
985
|
-
const logWritten = await appendLogSafe({
|
|
986
|
-
tool: "remote_stat",
|
|
987
|
-
server: input.server,
|
|
988
|
-
path: input.path,
|
|
989
|
-
approvalStatus: "not-required",
|
|
990
|
-
code: "SSH_STAT_FAILED",
|
|
991
|
-
message: payload.message,
|
|
992
|
-
})
|
|
993
|
-
return withAuditFlag("error", payload, logWritten)
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const remoteFind = async (input: RemoteFindInput): Promise<ToolResult<ExecResult>> => {
|
|
998
|
-
const logReady = await preflightLog<ExecResult>("remote_find", input.server)
|
|
999
|
-
if (logReady) {
|
|
1000
|
-
return logReady
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const resolved = await resolveServer(
|
|
1004
|
-
"remote_find",
|
|
1005
|
-
input.server,
|
|
1006
|
-
{ path: input.path, pattern: input.pattern, glob: input.glob, limit: input.limit },
|
|
1007
|
-
"not-required",
|
|
1008
|
-
)
|
|
1009
|
-
if (resolved.result) {
|
|
1010
|
-
return resolved.result as ToolResult<ExecResult>
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
const limit = clampLimit(input.limit, 200)
|
|
1014
|
-
const command = input.glob
|
|
1015
|
-
? `find ${quoteShell(input.path)} -name ${quoteShell(input.glob)} | head -n ${limit}`
|
|
1016
|
-
: `grep -R -n ${quoteShell(input.pattern)} ${quoteShell(input.path)} | head -n ${limit}`
|
|
1017
|
-
|
|
1018
|
-
try {
|
|
1019
|
-
const connection = toConnectConfig("remote_find", resolved.server!)
|
|
1020
|
-
if ("status" in connection) {
|
|
1021
|
-
return logAuthFailure(
|
|
1022
|
-
"remote_find",
|
|
1023
|
-
"not-required",
|
|
1024
|
-
{ path: input.path, pattern: input.pattern, glob: input.glob, limit: input.limit },
|
|
1025
|
-
connection,
|
|
1026
|
-
)
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const executed = await ssh.exec(connection, command)
|
|
1030
|
-
const payload: ToolPayload<ExecResult> = {
|
|
1031
|
-
tool: "remote_find",
|
|
1032
|
-
server: input.server,
|
|
1033
|
-
data: executed,
|
|
1034
|
-
execution: {
|
|
1035
|
-
attempted: true,
|
|
1036
|
-
completed: true,
|
|
1037
|
-
exitCode: executed.exitCode,
|
|
1038
|
-
stdoutBytes: byteLength(executed.stdout),
|
|
1039
|
-
stderrBytes: byteLength(executed.stderr),
|
|
1040
|
-
},
|
|
1041
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
1042
|
-
}
|
|
1043
|
-
const logWritten = await appendLogSafe({
|
|
1044
|
-
tool: "remote_find",
|
|
1045
|
-
server: input.server,
|
|
1046
|
-
command,
|
|
1047
|
-
approvalStatus: "not-required",
|
|
1048
|
-
...executed,
|
|
1049
|
-
})
|
|
1050
|
-
return withAuditFlag("ok", payload, logWritten)
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
const payload: ToolPayload<ExecResult> = {
|
|
1053
|
-
tool: "remote_find",
|
|
1054
|
-
server: input.server,
|
|
1055
|
-
code: "SSH_FIND_FAILED",
|
|
1056
|
-
message: (error as Error).message,
|
|
1057
|
-
execution: { attempted: true, completed: false },
|
|
1058
|
-
audit: { logWritten: false, snapshotStatus: "not-applicable" },
|
|
1059
|
-
}
|
|
1060
|
-
const logWritten = await appendLogSafe({
|
|
1061
|
-
tool: "remote_find",
|
|
1062
|
-
server: input.server,
|
|
1063
|
-
command,
|
|
1064
|
-
approvalStatus: "not-required",
|
|
1065
|
-
code: "SSH_FIND_FAILED",
|
|
1066
|
-
message: payload.message,
|
|
1067
|
-
})
|
|
1068
|
-
return withAuditFlag("error", payload, logWritten)
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
return {
|
|
1073
|
-
listServers,
|
|
1074
|
-
remoteExec,
|
|
1075
|
-
remoteReadFile,
|
|
1076
|
-
remoteWriteFile,
|
|
1077
|
-
remotePatchFile,
|
|
1078
|
-
remoteListDir,
|
|
1079
|
-
remoteStat,
|
|
1080
|
-
remoteFind,
|
|
1081
|
-
}
|
|
1082
|
-
}
|