@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.
Files changed (61) hide show
  1. package/dist/cli/openshell.js +4 -0
  2. package/dist/core/audit/log-store.js +1 -1
  3. package/dist/core/orchestrator.d.ts +2 -2
  4. package/dist/core/orchestrator.js +3 -3
  5. package/dist/core/result.d.ts +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/opencode/plugin.d.ts +1 -1
  9. package/dist/opencode/plugin.js +8 -8
  10. package/package.json +6 -1
  11. package/.claude/settings.local.json +0 -15
  12. package/bun.lock +0 -368
  13. package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
  14. package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
  15. package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
  16. package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
  17. package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
  18. package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
  19. package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
  20. package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
  21. package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
  22. package/examples/opencode-local/opencode.json +0 -19
  23. package/scripts/openshell.ts +0 -3
  24. package/scripts/server-registry.ts +0 -3
  25. package/src/cli/openshell.ts +0 -60
  26. package/src/cli/server-registry.ts +0 -476
  27. package/src/core/audit/git-audit-repo.ts +0 -42
  28. package/src/core/audit/log-store.ts +0 -20
  29. package/src/core/audit/redact.ts +0 -4
  30. package/src/core/contracts.ts +0 -51
  31. package/src/core/orchestrator.ts +0 -1082
  32. package/src/core/patch.ts +0 -11
  33. package/src/core/paths.ts +0 -32
  34. package/src/core/policy.ts +0 -30
  35. package/src/core/registry/server-registry.ts +0 -505
  36. package/src/core/result.ts +0 -16
  37. package/src/core/ssh/ssh-runtime.ts +0 -355
  38. package/src/index.ts +0 -3
  39. package/src/opencode/plugin.ts +0 -242
  40. package/src/product/install.ts +0 -43
  41. package/src/product/opencode-config.ts +0 -118
  42. package/src/product/uninstall.ts +0 -47
  43. package/src/product/workspace-tracker.ts +0 -69
  44. package/tests/integration/fake-ssh-server.ts +0 -97
  45. package/tests/integration/install-lifecycle.test.ts +0 -85
  46. package/tests/integration/orchestrator.test.ts +0 -767
  47. package/tests/integration/ssh-runtime.test.ts +0 -122
  48. package/tests/unit/audit.test.ts +0 -221
  49. package/tests/unit/build-layout.test.ts +0 -28
  50. package/tests/unit/opencode-config.test.ts +0 -100
  51. package/tests/unit/opencode-plugin.test.ts +0 -358
  52. package/tests/unit/openshell-cli.test.ts +0 -60
  53. package/tests/unit/paths.test.ts +0 -64
  54. package/tests/unit/plugin-export.test.ts +0 -10
  55. package/tests/unit/policy.test.ts +0 -53
  56. package/tests/unit/release-docs.test.ts +0 -31
  57. package/tests/unit/result.test.ts +0 -28
  58. package/tests/unit/server-registry-cli.test.ts +0 -673
  59. package/tests/unit/server-registry.test.ts +0 -452
  60. package/tests/unit/workspace-tracker.test.ts +0 -57
  61. package/tsconfig.json +0 -14
@@ -1,767 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import { tmpdir } from "node:os"
5
- import { createOrchestrator } from "../../src/core/orchestrator"
6
- import type { ResolvedServerRecord, ServerRecord } from "../../src/core/registry/server-registry"
7
-
8
- const createServerRecord = (id: string): ServerRecord => ({
9
- id,
10
- host: `${id}.example`,
11
- port: 22,
12
- username: "open",
13
- auth: {
14
- kind: "password",
15
- secret: "openpass",
16
- },
17
- })
18
-
19
- const createResolvedServerRecord = (
20
- overrides: Partial<ResolvedServerRecord> & Pick<ResolvedServerRecord, "scope">,
21
- ): ResolvedServerRecord => ({
22
- id: "prod-a",
23
- host: "prod-a.example",
24
- port: 22,
25
- username: "open",
26
- auth: {
27
- kind: "password",
28
- secret: "openpass",
29
- },
30
- workspaceRoot: "/repo",
31
- ...overrides,
32
- })
33
-
34
- const createStubSsh = (overrides: Partial<ReturnType<typeof createStubSshBase>> = {}) => ({
35
- ...createStubSshBase(),
36
- ...overrides,
37
- })
38
-
39
- const createStubSshBase = () => ({
40
- exec: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
41
- readFile: async () => "",
42
- writeFile: async () => {},
43
- listDir: async () => [] as string[] | { name: string; longname: string }[],
44
- stat: async () => ({ size: 0, mode: 0o644, isFile: false, isDirectory: false }),
45
- })
46
-
47
- const createStubAudit = (overrides: Partial<ReturnType<typeof createStubAuditBase>> = {}) => ({
48
- ...createStubAuditBase(),
49
- ...overrides,
50
- })
51
-
52
- const createStubAuditBase = () => ({
53
- preflightLog: async () => {},
54
- appendLog: async () => {},
55
- preflightSnapshots: async () => {},
56
- captureSnapshots: async () => {},
57
- })
58
-
59
- describe("tool orchestrator", () => {
60
- test("auto-allows safe remote exec commands", async () => {
61
- const logs: Record<string, unknown>[] = []
62
- const orchestrator = createOrchestrator({
63
- registry: { list: async () => [], resolve: async () => createServerRecord("prod-a") },
64
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
65
- ssh: createStubSsh({
66
- exec: async (_server, _command, options) => ({
67
- stdout: options?.cwd ?? "",
68
- stderr: "",
69
- exitCode: options?.timeout ?? 0,
70
- }),
71
- }),
72
- audit: createStubAudit({
73
- appendLog: async (entry) => {
74
- logs.push(entry)
75
- },
76
- }),
77
- })
78
-
79
- const result = await orchestrator.remoteExec({
80
- server: "prod-a",
81
- command: "cat /etc/hosts",
82
- cwd: "/etc",
83
- timeout: 5000,
84
- })
85
-
86
- expect(result).toMatchObject({ status: "ok", data: { stdout: "/etc", exitCode: 5000 } })
87
- expect(logs).toEqual([
88
- expect.objectContaining({
89
- tool: "remote_exec",
90
- server: "prod-a",
91
- approvalStatus: "not-required",
92
- policyDecision: "auto-allow",
93
- }),
94
- ])
95
- })
96
-
97
- test("executes approval-required remote exec commands with host-managed approval metadata", async () => {
98
- const logs: Record<string, unknown>[] = []
99
- const orchestrator = createOrchestrator({
100
- registry: { list: async () => [], resolve: async () => createServerRecord("prod-a") },
101
- policy: { classifyRemoteExec: () => ({ decision: "approval-required", reason: "write" }) },
102
- ssh: createStubSsh({
103
- exec: async () => ({ stdout: "ok", stderr: "", exitCode: 0 }),
104
- }),
105
- audit: createStubAudit({
106
- appendLog: async (entry) => {
107
- logs.push(entry)
108
- },
109
- }),
110
- })
111
-
112
- const result = await orchestrator.remoteExec({
113
- server: "prod-a",
114
- command: "touch /tmp/file",
115
- })
116
-
117
- expect(result).toMatchObject({ status: "ok", data: { stdout: "ok", exitCode: 0 } })
118
- expect(logs).toEqual([
119
- expect.objectContaining({
120
- tool: "remote_exec",
121
- server: "prod-a",
122
- approvalStatus: "host-managed-required",
123
- approvalRequired: true,
124
- policyDecision: "approval-required",
125
- }),
126
- ])
127
- })
128
-
129
- test("remote_exec reads workspace-relative privateKeyPath values at runtime", async () => {
130
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-auth-"))
131
- const workspaceRoot = join(tempDir, "repo")
132
- const keyPath = join(workspaceRoot, "keys", "id_rsa")
133
- const execCalls: Array<{ connection: Record<string, unknown>; command: string }> = []
134
-
135
- try {
136
- await mkdir(join(workspaceRoot, "keys"), { recursive: true })
137
- await writeFile(keyPath, "PRIVATE KEY")
138
-
139
- const orchestrator = createOrchestrator({
140
- registry: {
141
- list: async () => [],
142
- resolve: async () =>
143
- createResolvedServerRecord({
144
- scope: "workspace",
145
- workspaceRoot,
146
- auth: {
147
- kind: "privateKey",
148
- privateKeyPath: "keys/id_rsa",
149
- },
150
- }),
151
- },
152
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
153
- ssh: createStubSsh({
154
- exec: async (connection, command) => {
155
- execCalls.push({ connection: connection as Record<string, unknown>, command })
156
- return { stdout: "ok", stderr: "", exitCode: 0 }
157
- },
158
- }),
159
- audit: createStubAudit(),
160
- })
161
-
162
- const result = await orchestrator.remoteExec({
163
- server: "prod-a",
164
- command: "cat /etc/hosts",
165
- })
166
-
167
- expect(result).toMatchObject({ status: "ok", data: { stdout: "ok", exitCode: 0 } })
168
- expect(execCalls).toHaveLength(1)
169
- expect(execCalls[0]?.connection).toMatchObject({
170
- host: "prod-a.example",
171
- username: "open",
172
- privateKey: "PRIVATE KEY",
173
- })
174
- } finally {
175
- await rm(tempDir, { recursive: true, force: true })
176
- }
177
- })
178
-
179
- test("remote_exec forwards privateKey passphrase into ssh2 connect config", async () => {
180
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-auth-"))
181
- const workspaceRoot = join(tempDir, "repo")
182
- const keyPath = join(workspaceRoot, "keys", "id_rsa")
183
- const execCalls: Array<{ connection: Record<string, unknown>; command: string }> = []
184
-
185
- try {
186
- await mkdir(join(workspaceRoot, "keys"), { recursive: true })
187
- await writeFile(keyPath, "PRIVATE KEY")
188
-
189
- const orchestrator = createOrchestrator({
190
- registry: {
191
- list: async () => [],
192
- resolve: async () =>
193
- createResolvedServerRecord({
194
- scope: "workspace",
195
- workspaceRoot,
196
- auth: {
197
- kind: "privateKey",
198
- privateKeyPath: "keys/id_rsa",
199
- passphrase: "key-passphrase",
200
- },
201
- }),
202
- },
203
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
204
- ssh: createStubSsh({
205
- exec: async (connection, command) => {
206
- execCalls.push({ connection: connection as Record<string, unknown>, command })
207
- return { stdout: "ok", stderr: "", exitCode: 0 }
208
- },
209
- }),
210
- audit: createStubAudit(),
211
- })
212
-
213
- const result = await orchestrator.remoteExec({
214
- server: "prod-a",
215
- command: "cat /etc/hosts",
216
- })
217
-
218
- expect(result).toMatchObject({ status: "ok", data: { stdout: "ok", exitCode: 0 } })
219
- expect(execCalls).toHaveLength(1)
220
- expect(execCalls[0]?.connection).toMatchObject({
221
- host: "prod-a.example",
222
- username: "open",
223
- privateKey: "PRIVATE KEY",
224
- passphrase: "key-passphrase",
225
- })
226
- } finally {
227
- await rm(tempDir, { recursive: true, force: true })
228
- }
229
- })
230
-
231
- test("remote_exec forwards certificate passphrase into ssh2 connect config", async () => {
232
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-auth-"))
233
- const workspaceRoot = join(tempDir, "repo")
234
- const certificatePath = join(workspaceRoot, "certs", "client-cert.pem")
235
- const privateKeyPath = join(workspaceRoot, "keys", "client-key.pem")
236
- const execCalls: Array<{ connection: Record<string, unknown>; command: string }> = []
237
-
238
- try {
239
- await mkdir(join(workspaceRoot, "certs"), { recursive: true })
240
- await mkdir(join(workspaceRoot, "keys"), { recursive: true })
241
- await writeFile(certificatePath, "CERTIFICATE")
242
- await writeFile(privateKeyPath, "PRIVATE KEY")
243
-
244
- const orchestrator = createOrchestrator({
245
- registry: {
246
- list: async () => [],
247
- resolve: async () =>
248
- createResolvedServerRecord({
249
- scope: "workspace",
250
- workspaceRoot,
251
- auth: {
252
- kind: "certificate",
253
- certificatePath: "certs/client-cert.pem",
254
- privateKeyPath: "keys/client-key.pem",
255
- passphrase: "cert-passphrase",
256
- },
257
- }),
258
- },
259
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
260
- ssh: createStubSsh({
261
- exec: async (connection, command) => {
262
- execCalls.push({ connection: connection as Record<string, unknown>, command })
263
- return { stdout: "ok", stderr: "", exitCode: 0 }
264
- },
265
- }),
266
- audit: createStubAudit(),
267
- })
268
-
269
- const result = await orchestrator.remoteExec({
270
- server: "prod-a",
271
- command: "cat /etc/hosts",
272
- })
273
-
274
- expect(result).toMatchObject({ status: "ok", data: { stdout: "ok", exitCode: 0 } })
275
- expect(execCalls).toHaveLength(1)
276
- expect(execCalls[0]?.connection).toMatchObject({
277
- host: "prod-a.example",
278
- username: "open",
279
- privateKey: "PRIVATE KEY",
280
- passphrase: "cert-passphrase",
281
- })
282
- } finally {
283
- await rm(tempDir, { recursive: true, force: true })
284
- }
285
- })
286
-
287
- test("missing privateKeyPath returns KEY_PATH_NOT_FOUND before SSH execution", async () => {
288
- const logs: Record<string, unknown>[] = []
289
- let execCalled = false
290
- const orchestrator = createOrchestrator({
291
- registry: {
292
- list: async () => [],
293
- resolve: async () =>
294
- createResolvedServerRecord({
295
- scope: "workspace",
296
- workspaceRoot: "/workspace",
297
- auth: {
298
- kind: "privateKey",
299
- privateKeyPath: "keys/missing-id_rsa",
300
- },
301
- }),
302
- },
303
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
304
- ssh: createStubSsh({
305
- exec: async () => {
306
- execCalled = true
307
- return { stdout: "ok", stderr: "", exitCode: 0 }
308
- },
309
- }),
310
- audit: createStubAudit({
311
- appendLog: async (entry) => {
312
- logs.push(entry)
313
- },
314
- }),
315
- })
316
-
317
- const result = await orchestrator.remoteExec({
318
- server: "prod-a",
319
- command: "cat /etc/hosts",
320
- })
321
-
322
- expect(execCalled).toBe(false)
323
- expect(result).toMatchObject({
324
- status: "error",
325
- code: "KEY_PATH_NOT_FOUND",
326
- execution: { attempted: false, completed: false },
327
- audit: { logWritten: true, snapshotStatus: "not-applicable" },
328
- })
329
- expect(logs).toEqual([
330
- expect.objectContaining({
331
- tool: "remote_exec",
332
- server: "prod-a",
333
- approvalStatus: "not-required",
334
- code: "KEY_PATH_NOT_FOUND",
335
- }),
336
- ])
337
- })
338
-
339
- test("missing certificatePath returns CERTIFICATE_PATH_NOT_FOUND before SSH execution", async () => {
340
- const logs: Record<string, unknown>[] = []
341
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-auth-"))
342
- const workspaceRoot = join(tempDir, "repo")
343
- const privateKeyPath = join(workspaceRoot, "keys", "client-key.pem")
344
- let execCalled = false
345
-
346
- try {
347
- await mkdir(join(workspaceRoot, "keys"), { recursive: true })
348
- await writeFile(privateKeyPath, "PRIVATE KEY")
349
-
350
- const orchestrator = createOrchestrator({
351
- registry: {
352
- list: async () => [],
353
- resolve: async () =>
354
- createResolvedServerRecord({
355
- scope: "workspace",
356
- workspaceRoot,
357
- auth: {
358
- kind: "certificate",
359
- certificatePath: "certs/client-cert.pem",
360
- privateKeyPath: "keys/client-key.pem",
361
- },
362
- }),
363
- },
364
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
365
- ssh: createStubSsh({
366
- exec: async () => {
367
- execCalled = true
368
- return { stdout: "ok", stderr: "", exitCode: 0 }
369
- },
370
- }),
371
- audit: createStubAudit({
372
- appendLog: async (entry) => {
373
- logs.push(entry)
374
- },
375
- }),
376
- })
377
-
378
- const result = await orchestrator.remoteExec({
379
- server: "prod-a",
380
- command: "cat /etc/hosts",
381
- })
382
-
383
- expect(execCalled).toBe(false)
384
- expect(result).toMatchObject({
385
- status: "error",
386
- code: "CERTIFICATE_PATH_NOT_FOUND",
387
- execution: { attempted: false, completed: false },
388
- audit: { logWritten: true, snapshotStatus: "not-applicable" },
389
- })
390
- expect(logs).toEqual([
391
- expect.objectContaining({
392
- tool: "remote_exec",
393
- server: "prod-a",
394
- approvalStatus: "not-required",
395
- code: "CERTIFICATE_PATH_NOT_FOUND",
396
- }),
397
- ])
398
- } finally {
399
- await rm(tempDir, { recursive: true, force: true })
400
- }
401
- })
402
-
403
- test("malformed privateKey registry records fail before SSH execution", async () => {
404
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-registry-"))
405
- const workspaceRoot = join(tempDir, "repo")
406
- const globalRegistryFile = join(tempDir, "config", "servers.json")
407
- const workspaceRegistryFile = join(workspaceRoot, ".open-code", "servers.json")
408
-
409
- try {
410
- await mkdir(join(tempDir, "config"), { recursive: true })
411
- await mkdir(join(workspaceRoot, ".open-code"), { recursive: true })
412
- await writeFile(
413
- workspaceRegistryFile,
414
- JSON.stringify(
415
- [
416
- {
417
- id: "prod-a",
418
- host: "prod-a.example",
419
- port: 22,
420
- username: "open",
421
- auth: { kind: "privateKey" },
422
- },
423
- ],
424
- null,
425
- 2,
426
- ),
427
- )
428
-
429
- const { createServerRegistry } = await import("../../src/core/registry/server-registry?validation")
430
- const orchestrator = createOrchestrator({
431
- registry: createServerRegistry({
432
- globalRegistryFile,
433
- workspaceRegistryFile,
434
- workspaceRoot,
435
- }),
436
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
437
- ssh: createStubSsh({
438
- exec: async () => {
439
- throw new Error("ssh should not be called for malformed registry records")
440
- },
441
- }),
442
- audit: createStubAudit(),
443
- })
444
-
445
- const result = await orchestrator.remoteExec({
446
- server: "prod-a",
447
- command: "cat /etc/hosts",
448
- })
449
-
450
- expect(result).toMatchObject({
451
- status: "error",
452
- code: "REGISTRY_RECORD_INVALID",
453
- execution: { attempted: false, completed: false },
454
- })
455
- } finally {
456
- await rm(tempDir, { recursive: true, force: true })
457
- }
458
- })
459
-
460
- test("directory auth paths are reported as AUTH_PATH_UNREADABLE", async () => {
461
- const logs: Record<string, unknown>[] = []
462
- const tempDir = await mkdtemp(join(tmpdir(), "open-code-auth-"))
463
- const workspaceRoot = join(tempDir, "repo")
464
- let execCalled = false
465
-
466
- try {
467
- await mkdir(workspaceRoot, { recursive: true })
468
-
469
- const orchestrator = createOrchestrator({
470
- registry: {
471
- list: async () => [],
472
- resolve: async () =>
473
- createResolvedServerRecord({
474
- scope: "workspace",
475
- workspaceRoot,
476
- auth: {
477
- kind: "privateKey",
478
- privateKeyPath: ".",
479
- },
480
- }),
481
- },
482
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
483
- ssh: createStubSsh({
484
- exec: async () => {
485
- execCalled = true
486
- return { stdout: "ok", stderr: "", exitCode: 0 }
487
- },
488
- }),
489
- audit: createStubAudit({
490
- appendLog: async (entry) => {
491
- logs.push(entry)
492
- },
493
- }),
494
- })
495
-
496
- const result = await orchestrator.remoteExec({
497
- server: "prod-a",
498
- command: "cat /etc/hosts",
499
- })
500
-
501
- expect(execCalled).toBe(false)
502
- expect(result).toMatchObject({
503
- status: "error",
504
- code: "AUTH_PATH_UNREADABLE",
505
- execution: { attempted: false, completed: false },
506
- audit: { logWritten: true, snapshotStatus: "not-applicable" },
507
- })
508
- expect(logs).toEqual([
509
- expect.objectContaining({
510
- tool: "remote_exec",
511
- server: "prod-a",
512
- approvalStatus: "not-required",
513
- code: "AUTH_PATH_UNREADABLE",
514
- }),
515
- ])
516
- } finally {
517
- await rm(tempDir, { recursive: true, force: true })
518
- }
519
- })
520
-
521
- test("global relative auth paths are rejected before SSH execution", async () => {
522
- let execCalled = false
523
- const orchestrator = createOrchestrator({
524
- registry: {
525
- list: async () => [],
526
- resolve: async () =>
527
- createResolvedServerRecord({
528
- scope: "global",
529
- workspaceRoot: undefined,
530
- auth: {
531
- kind: "privateKey",
532
- privateKeyPath: "./keys/id_rsa",
533
- },
534
- }),
535
- },
536
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
537
- ssh: createStubSsh({
538
- exec: async () => {
539
- execCalled = true
540
- return { stdout: "ok", stderr: "", exitCode: 0 }
541
- },
542
- }),
543
- audit: createStubAudit(),
544
- })
545
-
546
- const result = await orchestrator.remoteExec({
547
- server: "prod-a",
548
- command: "cat /etc/hosts",
549
- })
550
-
551
- expect(execCalled).toBe(false)
552
- expect(result).toMatchObject({
553
- status: "error",
554
- code: "AUTH_PATH_INVALID",
555
- execution: { attempted: false, completed: false },
556
- })
557
- })
558
-
559
- test("returns structured not-found errors for reads", async () => {
560
- const logs: Record<string, unknown>[] = []
561
- const orchestrator = createOrchestrator({
562
- registry: { list: async () => [], resolve: async () => null },
563
- ssh: createStubSsh(),
564
- audit: createStubAudit({
565
- appendLog: async (entry) => {
566
- logs.push(entry)
567
- },
568
- }),
569
- })
570
-
571
- const result = await orchestrator.remoteReadFile({
572
- server: "missing",
573
- path: "/etc/hosts",
574
- })
575
-
576
- expect(result).toMatchObject({
577
- status: "error",
578
- tool: "remote_read_file",
579
- server: "missing",
580
- code: "SERVER_NOT_FOUND",
581
- execution: { attempted: false, completed: false },
582
- audit: { logWritten: true, snapshotStatus: "not-applicable" },
583
- })
584
- expect(logs).toEqual([
585
- expect.objectContaining({
586
- tool: "remote_read_file",
587
- server: "missing",
588
- code: "SERVER_NOT_FOUND",
589
- }),
590
- ])
591
- })
592
-
593
- test("slices remote file reads with offset and length", async () => {
594
- const orchestrator = createOrchestrator({
595
- registry: { list: async () => [], resolve: async () => createServerRecord("prod-a") },
596
- ssh: createStubSsh({
597
- readFile: async () => "abcdef",
598
- }),
599
- audit: createStubAudit(),
600
- })
601
-
602
- const result = await orchestrator.remoteReadFile({
603
- server: "prod-a",
604
- path: "/tmp/example.txt",
605
- offset: 2,
606
- length: 2,
607
- })
608
-
609
- expect(result).toMatchObject({
610
- status: "ok",
611
- data: { content: "cd" },
612
- })
613
- })
614
-
615
- test("returns partial failure when audit snapshot finalization fails after a successful write", async () => {
616
- const orchestrator = createOrchestrator({
617
- registry: { list: async () => [], resolve: async () => createServerRecord("prod-a") },
618
- ssh: createStubSsh({
619
- readFile: async () => "port=80\n",
620
- writeFile: async () => {},
621
- }),
622
- audit: createStubAudit({
623
- captureSnapshots: async () => {
624
- throw new Error("git commit failed")
625
- },
626
- }),
627
- })
628
-
629
- const result = await orchestrator.remoteWriteFile({
630
- server: "prod-a",
631
- path: "/tmp/app.conf",
632
- content: "port=81\n",
633
- mode: 0o640,
634
- })
635
-
636
- expect(result.status).toBe("partial_failure")
637
- expect(result.execution).toMatchObject({ attempted: true, completed: true })
638
- expect(result.audit).toMatchObject({ logWritten: true, snapshotStatus: "partial-failure" })
639
- })
640
-
641
- test("keeps execution and audit scoped to the addressed server", async () => {
642
- const logs: Record<string, unknown>[] = []
643
- const orchestrator = createOrchestrator({
644
- registry: {
645
- list: async () => [],
646
- resolve: async (id: string) => createServerRecord(id),
647
- },
648
- policy: { classifyRemoteExec: () => ({ decision: "auto-allow", reason: "safe inspection command" }) },
649
- ssh: createStubSsh({
650
- exec: async (server) => ({ stdout: server.host, stderr: "", exitCode: 0 }),
651
- }),
652
- audit: createStubAudit({
653
- appendLog: async (entry) => {
654
- logs.push(entry)
655
- },
656
- }),
657
- })
658
-
659
- const first = await orchestrator.remoteExec({ server: "prod-a", command: "pwd" })
660
- const second = await orchestrator.remoteExec({ server: "prod-b", command: "pwd" })
661
-
662
- expect(first.data).toMatchObject({ stdout: "prod-a.example" })
663
- expect(second.data).toMatchObject({ stdout: "prod-b.example" })
664
- expect(logs.map((entry) => entry.server)).toEqual(["prod-a", "prod-b"])
665
- })
666
-
667
- test("keeps file writes and snapshots partitioned across two registered servers", async () => {
668
- const snapshots: Record<string, unknown>[] = []
669
- const files = new Map([
670
- ["prod-a.example:/tmp/app.conf", "port=80\n"],
671
- ["prod-b.example:/tmp/app.conf", "port=90\n"],
672
- ])
673
-
674
- const orchestrator = createOrchestrator({
675
- registry: {
676
- list: async () => [],
677
- resolve: async (id: string) => createServerRecord(id),
678
- },
679
- ssh: createStubSsh({
680
- readFile: async (server, path) => files.get(`${server.host}:${path}`) ?? "",
681
- writeFile: async (server, path, content) => {
682
- files.set(`${server.host}:${path}`, content)
683
- },
684
- }),
685
- audit: createStubAudit({
686
- captureSnapshots: async (entry) => {
687
- snapshots.push(entry)
688
- },
689
- }),
690
- })
691
-
692
- await orchestrator.remoteWriteFile({ server: "prod-a", path: "/tmp/app.conf", content: "port=81\n" })
693
- await orchestrator.remoteWriteFile({ server: "prod-b", path: "/tmp/app.conf", content: "port=91\n" })
694
-
695
- expect(snapshots).toEqual([
696
- expect.objectContaining({ server: "prod-a", path: "/tmp/app.conf", before: "port=80\n", after: "port=81\n" }),
697
- expect.objectContaining({ server: "prod-b", path: "/tmp/app.conf", before: "port=90\n", after: "port=91\n" }),
698
- ])
699
- })
700
-
701
- test("lists servers without exposing auth material", async () => {
702
- const orchestrator = createOrchestrator({
703
- registry: {
704
- list: async () => [
705
- createResolvedServerRecord({
706
- scope: "workspace",
707
- }),
708
- ],
709
- resolve: async () => createServerRecord("prod-a"),
710
- },
711
- ssh: createStubSsh(),
712
- audit: createStubAudit(),
713
- })
714
-
715
- const result = await orchestrator.listServers()
716
-
717
- expect(result.status).toBe("ok")
718
- expect(result.data).toEqual([
719
- expect.objectContaining({
720
- id: "prod-a",
721
- host: "prod-a.example",
722
- }),
723
- ])
724
- expect((result.data as Array<Record<string, unknown>>)[0]).not.toHaveProperty("auth")
725
- expect((result.data as Array<Record<string, unknown>>)[0]).not.toHaveProperty("workspaceRoot")
726
- })
727
-
728
- test("builds remote find commands without reusing remote_exec policy gating", async () => {
729
- const logs: Record<string, unknown>[] = []
730
- let executedCommand = ""
731
- const orchestrator = createOrchestrator({
732
- registry: {
733
- list: async () => [],
734
- resolve: async () => createServerRecord("prod-a"),
735
- },
736
- policy: { classifyRemoteExec: () => ({ decision: "reject", reason: "should not be used here" }) },
737
- ssh: createStubSsh({
738
- exec: async (_server, command) => {
739
- executedCommand = command
740
- return { stdout: "match", stderr: "", exitCode: 0 }
741
- },
742
- }),
743
- audit: createStubAudit({
744
- appendLog: async (entry) => {
745
- logs.push(entry)
746
- },
747
- }),
748
- })
749
-
750
- const result = await orchestrator.remoteFind({
751
- server: "prod-a",
752
- path: "/var/log",
753
- pattern: "ERROR",
754
- limit: 5,
755
- })
756
-
757
- expect(result).toMatchObject({ status: "ok", data: { stdout: "match", exitCode: 0 } })
758
- expect(executedCommand).toContain("grep -R -n")
759
- expect(logs).toEqual([
760
- expect.objectContaining({
761
- tool: "remote_find",
762
- server: "prod-a",
763
- approvalStatus: "not-required",
764
- }),
765
- ])
766
- })
767
- })