@junwu168/openshell 0.1.0

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 (91) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/README.md +63 -0
  3. package/bun.lock +368 -0
  4. package/dist/cli/openshell.d.ts +13 -0
  5. package/dist/cli/openshell.js +41 -0
  6. package/dist/cli/server-registry.d.ts +22 -0
  7. package/dist/cli/server-registry.js +349 -0
  8. package/dist/core/audit/git-audit-repo.d.ts +10 -0
  9. package/dist/core/audit/git-audit-repo.js +38 -0
  10. package/dist/core/audit/log-store.d.ts +4 -0
  11. package/dist/core/audit/log-store.js +17 -0
  12. package/dist/core/audit/redact.d.ts +1 -0
  13. package/dist/core/audit/redact.js +3 -0
  14. package/dist/core/contracts.d.ts +28 -0
  15. package/dist/core/contracts.js +1 -0
  16. package/dist/core/orchestrator.d.ts +110 -0
  17. package/dist/core/orchestrator.js +825 -0
  18. package/dist/core/patch.d.ts +1 -0
  19. package/dist/core/patch.js +8 -0
  20. package/dist/core/paths.d.ts +26 -0
  21. package/dist/core/paths.js +26 -0
  22. package/dist/core/policy.d.ts +16 -0
  23. package/dist/core/policy.js +29 -0
  24. package/dist/core/registry/server-registry.d.ts +59 -0
  25. package/dist/core/registry/server-registry.js +350 -0
  26. package/dist/core/result.d.ts +4 -0
  27. package/dist/core/result.js +12 -0
  28. package/dist/core/ssh/ssh-runtime.d.ts +31 -0
  29. package/dist/core/ssh/ssh-runtime.js +240 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +3 -0
  32. package/dist/opencode/plugin.d.ts +10 -0
  33. package/dist/opencode/plugin.js +183 -0
  34. package/dist/product/install.d.ts +12 -0
  35. package/dist/product/install.js +25 -0
  36. package/dist/product/opencode-config.d.ts +15 -0
  37. package/dist/product/opencode-config.js +93 -0
  38. package/dist/product/uninstall.d.ts +12 -0
  39. package/dist/product/uninstall.js +27 -0
  40. package/dist/product/workspace-tracker.d.ts +11 -0
  41. package/dist/product/workspace-tracker.js +48 -0
  42. package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +81 -0
  43. package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +174 -0
  44. package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +1656 -0
  45. package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +54 -0
  46. package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +494 -0
  47. package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +639 -0
  48. package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +378 -0
  49. package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +272 -0
  50. package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +197 -0
  51. package/examples/opencode-local/opencode.json +19 -0
  52. package/package.json +33 -0
  53. package/scripts/openshell.ts +3 -0
  54. package/scripts/server-registry.ts +3 -0
  55. package/src/cli/openshell.ts +59 -0
  56. package/src/cli/server-registry.ts +470 -0
  57. package/src/core/audit/git-audit-repo.ts +42 -0
  58. package/src/core/audit/log-store.ts +20 -0
  59. package/src/core/audit/redact.ts +4 -0
  60. package/src/core/contracts.ts +51 -0
  61. package/src/core/orchestrator.ts +1082 -0
  62. package/src/core/patch.ts +11 -0
  63. package/src/core/paths.ts +32 -0
  64. package/src/core/policy.ts +30 -0
  65. package/src/core/registry/server-registry.ts +505 -0
  66. package/src/core/result.ts +16 -0
  67. package/src/core/ssh/ssh-runtime.ts +355 -0
  68. package/src/index.ts +3 -0
  69. package/src/opencode/plugin.ts +242 -0
  70. package/src/product/install.ts +43 -0
  71. package/src/product/opencode-config.ts +118 -0
  72. package/src/product/uninstall.ts +47 -0
  73. package/src/product/workspace-tracker.ts +69 -0
  74. package/tests/integration/fake-ssh-server.ts +97 -0
  75. package/tests/integration/install-lifecycle.test.ts +85 -0
  76. package/tests/integration/orchestrator.test.ts +767 -0
  77. package/tests/integration/ssh-runtime.test.ts +122 -0
  78. package/tests/unit/audit.test.ts +221 -0
  79. package/tests/unit/build-layout.test.ts +28 -0
  80. package/tests/unit/opencode-config.test.ts +100 -0
  81. package/tests/unit/opencode-plugin.test.ts +358 -0
  82. package/tests/unit/openshell-cli.test.ts +60 -0
  83. package/tests/unit/paths.test.ts +64 -0
  84. package/tests/unit/plugin-export.test.ts +10 -0
  85. package/tests/unit/policy.test.ts +53 -0
  86. package/tests/unit/release-docs.test.ts +31 -0
  87. package/tests/unit/result.test.ts +28 -0
  88. package/tests/unit/server-registry-cli.test.ts +673 -0
  89. package/tests/unit/server-registry.test.ts +452 -0
  90. package/tests/unit/workspace-tracker.test.ts +57 -0
  91. package/tsconfig.json +14 -0
@@ -0,0 +1,48 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ const readEntries = async (trackerFile) => {
4
+ try {
5
+ const raw = await readFile(trackerFile, "utf8");
6
+ const parsed = JSON.parse(raw);
7
+ if (!Array.isArray(parsed)) {
8
+ return [];
9
+ }
10
+ return parsed.filter((entry) => {
11
+ if (typeof entry !== "object" || entry === null) {
12
+ return false;
13
+ }
14
+ const candidate = entry;
15
+ return typeof candidate.workspaceRoot === "string" && typeof candidate.managedPath === "string";
16
+ });
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT") {
20
+ return [];
21
+ }
22
+ throw error;
23
+ }
24
+ };
25
+ const writeEntries = async (trackerFile, entries) => {
26
+ await mkdir(dirname(trackerFile), { recursive: true });
27
+ await writeFile(trackerFile, JSON.stringify(entries, null, 2) + "\n");
28
+ };
29
+ export const createWorkspaceTracker = (trackerFile) => ({
30
+ async list() {
31
+ return readEntries(trackerFile);
32
+ },
33
+ async record(entry) {
34
+ const entries = await readEntries(trackerFile);
35
+ const next = [
36
+ ...entries.filter((existing) => existing.workspaceRoot !== entry.workspaceRoot),
37
+ entry,
38
+ ];
39
+ await writeEntries(trackerFile, next);
40
+ },
41
+ async remove(workspaceRoot) {
42
+ const entries = await readEntries(trackerFile);
43
+ await writeEntries(trackerFile, entries.filter((entry) => entry.workspaceRoot !== workspaceRoot));
44
+ },
45
+ async clear() {
46
+ await writeEntries(trackerFile, []);
47
+ },
48
+ });
@@ -0,0 +1,81 @@
1
+ # OpenCode Remote Tools Handoff
2
+
3
+ Date: 2026-03-25
4
+ Execution mode: `subagent-driven-development`
5
+ Worktree: `.worktrees/opencode-v1`
6
+ Branch: `opencode-v1`
7
+
8
+ ## Source Docs
9
+
10
+ - Spec: `docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md`
11
+ - Plan: `docs/superpowers/plans/2026-03-25-opencode-remote-tools.md`
12
+
13
+ ## What Was Completed
14
+
15
+ - Tasks 1 through 9 from the implementation plan are complete.
16
+ - The OpenCode adapter now registers explicit remote tools:
17
+ - `list_servers`
18
+ - `remote_exec`
19
+ - `remote_read_file`
20
+ - `remote_write_file`
21
+ - `remote_patch_file`
22
+ - `remote_list_dir`
23
+ - `remote_stat`
24
+ - `remote_find`
25
+ - The local smoke fixture exists under `examples/opencode-local/`.
26
+ - Build output now aligns with package and smoke-fixture expectations by emitting `dist/index.js`.
27
+ - The plugin test no longer relies on checkout-specific absolute paths or global module-mock leakage.
28
+
29
+ ## Final Verified State
30
+
31
+ Verified on the latest branch head after the approval-gap fix:
32
+
33
+ - `~/.bun/bin/bun test tests/unit/*.test.ts tests/integration/ssh-runtime.test.ts tests/integration/orchestrator.test.ts`
34
+ - Result: `56 pass`, `0 fail`
35
+ - `~/.bun/bin/bun run typecheck`
36
+ - Result: pass
37
+ - `~/.bun/bin/bun run build`
38
+ - Result: pass
39
+
40
+ ## Smoke-Test Evidence
41
+
42
+ - `examples/opencode-local/.opencode/plugins/open-code.ts` resolves successfully against the built package.
43
+ - `opencode run --print-logs "Call list_servers and print the raw tool result."` loaded the local plugin and executed `list_servers` successfully.
44
+ - OpenCode logs showed the custom tools were registered, including `list_servers`, `remote_exec`, `remote_read_file`, `remote_write_file`, `remote_patch_file`, `remote_list_dir`, `remote_stat`, and `remote_find`.
45
+ - Unit coverage now verifies the approval handoff path:
46
+ - safe `remote_exec` does not call `context.ask(...)`
47
+ - approval-required `remote_exec` calls `context.ask(...)` with built-in `bash` permission
48
+ - `remote_write_file` calls `context.ask(...)` with built-in `edit` permission
49
+ - the local example `opencode.json` is aligned to `bash` / `edit`, not custom tool ids
50
+
51
+ ## Approval Gap Root Cause And Fix
52
+
53
+ - Root cause: OpenCode did not enforce `opencode.json` permission entries keyed by custom plugin tool names like `remote_write_file` or `remote_exec`.
54
+ - Evidence: interactive OpenCode allowed `remote_write_file` to run without a prompt and returned our structured `SERVER_NOT_FOUND` result.
55
+ - Fix: the adapter now explicitly requests approval through the plugin SDK `context.ask(...)` API before:
56
+ - approval-required `remote_exec`
57
+ - `remote_write_file`
58
+ - `remote_patch_file`
59
+ - The adapter requests approval under OpenCode's built-in permission families:
60
+ - `bash` for approval-required remote shell execution
61
+ - `edit` for remote file mutations
62
+ - The example config now uses `bash` and `edit` permission keys accordingly.
63
+
64
+ ## Residual Gap
65
+
66
+ - A fresh interactive manual re-check is still required on the latest head to confirm host-side prompt UX:
67
+ - safe `remote_exec` must not prompt
68
+ - `remote_write_file` must prompt before the tool returns `SERVER_NOT_FOUND` for a missing server
69
+ - The prior upstream OpenCode provider/certificate instability remains a possible source of noise for nontrivial end-to-end runs.
70
+
71
+ ## Recommended Starting Point For The Next Session
72
+
73
+ - Start from the pushed `opencode-v1` branch.
74
+ - If the next session is feature design, begin from the shipped v1 boundary in the spec and treat this branch as the implementation baseline.
75
+ - If the next session is verification-focused, first re-run the manual OpenCode smoke flow on the latest head:
76
+ 1. `bun run build`
77
+ 2. `cd examples/opencode-local`
78
+ 3. `opencode`
79
+ 4. confirm `list_servers`
80
+ 5. confirm safe `remote_exec`
81
+ 6. confirm `remote_write_file` shows approval prompt
@@ -0,0 +1,174 @@
1
+ # OpenShell Pre-Release Review Note
2
+
3
+ Date: 2026-03-26
4
+ Branch: `main`
5
+ Reviewer: Claude Code
6
+
7
+ ## Scope
8
+
9
+ This document reviews the first pre-release candidate for:
10
+
11
+ - npm package: `@junwu168/openshell`
12
+ - CLI binary: `openshell`
13
+ - supported host: `opencode`
14
+
15
+ The package enables AI coding CLIs to safely operate on remote Linux servers over SSH with credential isolation, user approval enforcement, and local audit trails.
16
+
17
+ ## Automated Verification
18
+
19
+ Verified on current HEAD:
20
+
21
+ ```bash
22
+ bun test
23
+ bun run typecheck
24
+ bun run build
25
+ ```
26
+
27
+ Observed result:
28
+
29
+ - `bun test` -> `96 pass, 0 fail`
30
+ - `typecheck` -> pass
31
+ - `build` -> pass
32
+
33
+ ## Architecture Summary
34
+
35
+ ```
36
+ src/
37
+ ├── index.ts # Package exports (OpenCodePlugin, contracts)
38
+ ├── core/ # Host-agnostic runtime core
39
+ │ ├── contracts.ts # Shared types: ServerID, ToolPayload, ToolResult, PolicyDecision
40
+ │ ├── result.ts # okResult(), partialFailureResult(), errorResult()
41
+ │ ├── paths.ts # Runtime path resolution via env-paths
42
+ │ ├── policy.ts # Deterministic command classification
43
+ │ ├── patch.ts # Unified diff application
44
+ │ ├── orchestrator.ts # Central pipeline: validate -> classify -> approve -> execute -> audit
45
+ │ ├── registry/ # Server registry (layered JSON, NOT encrypted)
46
+ │ │ └── server-registry.ts # Global + workspace scoped server records
47
+ │ ├── ssh/ # SSH/SFTP operations
48
+ │ │ └── ssh-runtime.ts # exec, readFile, writeFile, listDir, stat
49
+ │ └── audit/ # Audit logging and git-backed snapshots
50
+ │ ├── log-store.ts # JSONL append-only audit log
51
+ │ ├── git-audit-repo.ts # Git-backed file snapshots
52
+ │ └── redact.ts # Secret redaction before logging
53
+ ├── opencode/
54
+ │ └── plugin.ts # OpenCode adapter (tool definitions, approval prompts)
55
+ ├── cli/ # CLI commands
56
+ │ ├── openshell.ts # Main CLI entry (install/uninstall/server-registry)
57
+ │ └── server-registry.ts # Interactive server registry CLI
58
+ └── product/ # Install/uninstall lifecycle
59
+ ├── install.ts # openshell install
60
+ ├── uninstall.ts # openshell uninstall
61
+ ├── opencode-config.ts # OpenCode config merging
62
+ └── workspace-tracker.ts # Track workspaces for cleanup
63
+ ```
64
+
65
+ ## Implemented Features
66
+
67
+ ### Remote Tools (8 tools)
68
+ - `list_servers` - List registered servers
69
+ - `remote_exec` - Execute shell commands on remote servers
70
+ - `remote_read_file` - Read remote files
71
+ - `remote_write_file` - Write remote files (approval-required)
72
+ - `remote_patch_file` - Apply unified diffs (approval-required)
73
+ - `remote_list_dir` - List remote directories
74
+ - `remote_stat` - Stat remote paths
75
+ - `remote_find` - Search remote files/content
76
+
77
+ ### Policy Engine
78
+ - **Auto-allow:** Safe inspection commands (cat, grep, find, ls, pwd, uname, df, free, ps, systemctl status)
79
+ - **Approval-required:** Middleware commands (psql, mysql, redis-cli, kubectl, docker, helm, aws, gcloud, az) and shell composition (pipes, redirects, chaining)
80
+ - **Reject:** Empty commands
81
+
82
+ ### Server Registry
83
+ - **Layered configuration:** Global (`~/.config/openshell/servers.json`) and workspace (`<workspace>/.open-code/servers.json`)
84
+ - **File locking:** Prevents concurrent write corruption
85
+ - **Workspace shadowing:** Workspace entries override global entries with the same ID
86
+ - **Auth types:** Password, private key (with optional passphrase), certificate
87
+
88
+ ### Audit System
89
+ - **JSONL action log:** All tool actions logged with timestamps, sanitized secrets
90
+ - **Git-backed snapshots:** Before/after content for file writes stored in git commits
91
+ - **Fail-closed:** If audit preflight fails, operations do not proceed
92
+
93
+ ### Install/Uninstall Lifecycle
94
+ - `openshell install` - Creates dirs, merges OpenCode config with plugin + permissions
95
+ - `openshell uninstall` - Aggressively removes all OpenShell state and tracked workspace `.open-code/` dirs
96
+
97
+ ## Security Considerations
98
+
99
+ ### Known Security Model (Documented)
100
+
101
+ > "Password auth is stored in plain text. That is intentionally simple for this pre-release and not recommended for long-term production use."
102
+
103
+ ### Secret Handling
104
+ - Passwords stored in plain text JSON files (keytar dependency was removed)
105
+ - Private key paths and certificate paths are read from filesystem at runtime
106
+ - Secret redaction in audit logs covers URLs with embedded credentials and `password=`, `secret=`, `token=` patterns
107
+ - `list_servers` properly excludes auth data from returned server records
108
+
109
+ ### Credential Isolation
110
+ - Server IDs used in tool calls, not raw credentials
111
+ - Auth paths validated for workspace-scoped records only
112
+ - Relative auth paths rejected for global scope
113
+
114
+ ## Pre-Release Concerns
115
+
116
+ ### 1. Plain-Text Password Storage
117
+ The current implementation stores passwords in plain text JSON. For a production release, encryption at rest would be essential.
118
+
119
+ ### 2. `remote_find` Uses Shell Execution (Medium Risk)
120
+ The `remote_find` implementation builds shell commands (`find ... | head -n ...` or `grep -R -n ... | head -n ...`) which bypasses the policy engine's shell composition detection. Commands with pipes or redirects could be constructed via the `pattern` or `glob` arguments.
121
+
122
+ ### 3. No Connection Pooling/Reuse (Performance)
123
+ Each SSH operation creates a new connection. For high-frequency tool use, this could be inefficient.
124
+
125
+ ### 4. Uninstall Removes Workspace `.open-code/` Dirs
126
+ The uninstall is "aggressive" and removes ALL tracked workspace `.open-code/` directories. If users have other plugins or data in those directories, it would be lost.
127
+
128
+ ### 5. Bun Runtime Dependency
129
+ The project uses Bun as its runtime/package manager. Node.js compatibility would require additional work.
130
+
131
+ ### 6. No Connection Recovery (Reliability)
132
+ The audit system is fail-closed, but there's no retry logic or recovery for transient SSH failures.
133
+
134
+ ## Review Path
135
+
136
+ Reviewer flow:
137
+
138
+ 1. `npm install -g @junwu168/openshell`
139
+ 2. `openshell install`
140
+ 3. `openshell server-registry add`
141
+ 4. Launch `opencode`
142
+ 5. Exercise:
143
+ - `list_servers`
144
+ - safe `remote_exec`
145
+ - approval-gated `remote_write_file`
146
+ 6. `openshell uninstall`
147
+
148
+ ## Prior Fix (Verified in This Review)
149
+
150
+ During prior verification, an uninstall bug was fixed:
151
+
152
+ - if OpenCode config only contained `@junwu168/openshell`, uninstall preserved the plugin entry because the config writer spread `...current` back into the output when the filtered plugin list became empty
153
+
154
+ This is covered by `tests/unit/opencode-config.test.ts` and fixed in `src/product/opencode-config.ts`.
155
+
156
+ ## Test Coverage
157
+
158
+ | File | Approx Lines | Coverage Relevance |
159
+ |------|-------------|-------------------|
160
+ | `src/core/orchestrator.ts` | ~1080 | Central pipeline (critical) |
161
+ | `src/core/registry/server-registry.ts` | ~505 | Server registry |
162
+ | `src/cli/server-registry.ts` | ~470 | Interactive CLI |
163
+ | `src/core/ssh/ssh-runtime.ts` | ~355 | SSH operations |
164
+ | `src/opencode/plugin.ts` | ~243 | OpenCode adapter |
165
+ | `src/product/opencode-config.ts` | ~118 | Config lifecycle |
166
+
167
+ ## Verdict
168
+
169
+ **Ready for pre-release** with the documented concerns understood by users:
170
+
171
+ - Plain-text password storage is acceptable for pre-release evaluation
172
+ - `remote_find` shell composition bypass should be addressed before production
173
+ - All automated tests pass (96 tests)
174
+