pi-harness-runtime 1.1.425 → 1.1.426

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 (167) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/harness/e2e/api-key-resolver.ts +3 -2
  4. package/harness/notification-center.ts +2 -1
  5. package/harness/skills-plan/2026-07-12-herdr-moo-skills-adaptation-plan.md +3 -3
  6. package/harness/skills-plan/2026-07-12-minimax-prompt-step1-sync.md +3 -3
  7. package/harness/skills-plan/2026-07-12-public-skill-export-and-sync-design.md +4 -4
  8. package/harness/ssh-detach-interceptor.test.ts +3 -3
  9. package/harness/ssh-hang-recovery.test.ts +86 -0
  10. package/harness/ssh-hang-recovery.ts +48 -14
  11. package/index.ts +9 -9
  12. package/package.json +1 -1
  13. package/packages/a2a-adapter/package.json +1 -1
  14. package/packages/agent-policy/package.json +1 -1
  15. package/packages/architecture-generator/package.json +1 -1
  16. package/packages/auth/package.json +1 -1
  17. package/packages/autonomous-refactor/package.json +1 -1
  18. package/packages/autonomous-runtime/package.json +1 -1
  19. package/packages/cache-strategy/package.json +1 -1
  20. package/packages/capability-registry/package.json +1 -1
  21. package/packages/checkpoint/package.json +1 -1
  22. package/packages/cli-plugin-sdk/package.json +1 -1
  23. package/packages/clipboard/package.json +1 -1
  24. package/packages/clipboard-plugin/package.json +1 -1
  25. package/packages/code-generation/package.json +1 -1
  26. package/packages/code-review/package.json +1 -1
  27. package/packages/codex-adapter/package.json +1 -1
  28. package/packages/codex-watcher/package.json +1 -1
  29. package/packages/config-capture/package.json +1 -1
  30. package/packages/connection-runtime/package.json +1 -1
  31. package/packages/context-compiler/package.json +1 -1
  32. package/packages/context-discovery/package.json +1 -1
  33. package/packages/context-manager/package.json +1 -1
  34. package/packages/contracts/dist/delegation-store.js +2 -1
  35. package/packages/contracts/dist/workspace-env-format.d.ts +2 -2
  36. package/packages/contracts/dist/workspace-env-format.js +2 -2
  37. package/packages/contracts/dist/workspace-env.js +2 -1
  38. package/packages/contracts/package.json +1 -1
  39. package/packages/contracts/src/delegation-store.ts +2 -1
  40. package/packages/contracts/src/workspace-env-format.ts +2 -2
  41. package/packages/contracts/src/workspace-env.ts +2 -1
  42. package/packages/contracts/test/workspace-env.test.ts +6 -6
  43. package/packages/cookie-sanitizer/package.json +1 -1
  44. package/packages/cost-optimizer/package.json +1 -1
  45. package/packages/dependency-analyzer/package.json +1 -1
  46. package/packages/django-plugin/package.json +1 -1
  47. package/packages/doc-generator/package.json +1 -1
  48. package/packages/durable-review/package.json +1 -1
  49. package/packages/e2e-tester/README.md +1 -1
  50. package/packages/e2e-tester/package.json +1 -1
  51. package/packages/evaluation-engine/package.json +1 -1
  52. package/packages/evaluation-runner/package.json +1 -1
  53. package/packages/event-bus/package.json +1 -1
  54. package/packages/event-sink/package.json +1 -1
  55. package/packages/event-store/package.json +1 -1
  56. package/packages/experience-replay/package.json +1 -1
  57. package/packages/feedback-collector/package.json +1 -1
  58. package/packages/file-copy-helper/package.json +1 -1
  59. package/packages/framework-detector/package.json +1 -1
  60. package/packages/framework-plugin-sdk/package.json +1 -1
  61. package/packages/frappe-plugin/package.json +1 -1
  62. package/packages/generic-web-plugin/package.json +1 -1
  63. package/packages/health-monitor/package.json +1 -1
  64. package/packages/intent-analyzer/package.json +1 -1
  65. package/packages/jev-judge/dist/delegation-harness.d.ts.map +1 -1
  66. package/packages/jev-judge/dist/delegation-harness.js +2 -1
  67. package/packages/jev-judge/dist/delegation-harness.js.map +1 -1
  68. package/packages/jev-judge/dist/index.d.ts.map +1 -1
  69. package/packages/jev-judge/dist/index.js +2 -1
  70. package/packages/jev-judge/dist/index.js.map +1 -1
  71. package/packages/jev-judge/package.json +1 -1
  72. package/packages/jev-judge/src/delegation-harness.ts +2 -1
  73. package/packages/jev-judge/src/index.ts +2 -1
  74. package/packages/knowledge-graph/package.json +1 -1
  75. package/packages/knowledge-retrieval/package.json +1 -1
  76. package/packages/laravel-plugin/package.json +1 -1
  77. package/packages/learning-engine/package.json +1 -1
  78. package/packages/logging/package.json +1 -1
  79. package/packages/logging/test/classification.test.ts +2 -2
  80. package/packages/mcp-adapter/package.json +1 -1
  81. package/packages/memory-engine/package.json +1 -1
  82. package/packages/milestone-manager/package.json +1 -1
  83. package/packages/model-registry/package.json +1 -1
  84. package/packages/nextjs-plugin/package.json +1 -1
  85. package/packages/notification/dist/adapters/telegram-adapter.js +2 -2
  86. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  87. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  88. package/packages/notification/dist/notification-center.js +2 -1
  89. package/packages/notification/dist/notification-center.js.map +1 -1
  90. package/packages/notification/package.json +1 -1
  91. package/packages/notification/src/adapters/telegram-adapter.ts +2 -2
  92. package/packages/notification/src/notification-center.ts +2 -1
  93. package/packages/observability/package.json +1 -1
  94. package/packages/okf-indexer/package.json +1 -1
  95. package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
  96. package/packages/paperclip-integration/dist/index.js +3 -2
  97. package/packages/paperclip-integration/dist/index.js.map +1 -1
  98. package/packages/paperclip-integration/package.json +1 -1
  99. package/packages/paperclip-integration/src/index.ts +4 -2
  100. package/packages/performance-optimizer/package.json +1 -1
  101. package/packages/permission-mode/package.json +1 -1
  102. package/packages/privilege-broker/package.json +1 -1
  103. package/packages/project-analyzer/package.json +1 -1
  104. package/packages/project-bootstrap/package.json +1 -1
  105. package/packages/projection-engine/package.json +1 -1
  106. package/packages/prompt-compiler/package.json +1 -1
  107. package/packages/prompt-versioning/package.json +1 -1
  108. package/packages/provider-adapter-sdk/package.json +1 -1
  109. package/packages/provider-router/package.json +1 -1
  110. package/packages/provider-selector/package.json +1 -1
  111. package/packages/providers/package.json +1 -1
  112. package/packages/qdrant-skills/package.json +1 -1
  113. package/packages/quota-manager/package.json +1 -1
  114. package/packages/rate-limiter/package.json +1 -1
  115. package/packages/react-vite-plugin/package.json +1 -1
  116. package/packages/relay/package.json +1 -1
  117. package/packages/relay/test/relay-e2e.test.ts +4 -4
  118. package/packages/release-manager/package.json +1 -1
  119. package/packages/requirement-compiler/package.json +1 -1
  120. package/packages/runtime/package.json +1 -1
  121. package/packages/scheduler/package.json +1 -1
  122. package/packages/scheduler-adapter/package.json +1 -1
  123. package/packages/session/package.json +1 -1
  124. package/packages/session-api/package.json +1 -1
  125. package/packages/session-export/package.json +1 -1
  126. package/packages/shared-context/package.json +1 -1
  127. package/packages/skill-mcp-client/package.json +1 -1
  128. package/packages/skill-registry/package.json +1 -1
  129. package/packages/skills/dist/loader.d.ts.map +1 -1
  130. package/packages/skills/dist/loader.js +2 -1
  131. package/packages/skills/dist/loader.js.map +1 -1
  132. package/packages/skills/package.json +1 -1
  133. package/packages/skills/src/loader.ts +2 -1
  134. package/packages/sprint-planner/package.json +1 -1
  135. package/packages/subscription-engine/package.json +1 -1
  136. package/packages/task-compiler/package.json +1 -1
  137. package/packages/telegram/package.json +1 -1
  138. package/packages/tencentdb-memory/package.json +1 -1
  139. package/packages/tencentdb-sync/package.json +1 -1
  140. package/packages/test-data-generator/package.json +1 -1
  141. package/packages/test-generator/package.json +1 -1
  142. package/packages/todo-bd-sync/package.json +1 -1
  143. package/packages/token-estimation/package.json +1 -1
  144. package/packages/token-optimizer/package.json +1 -1
  145. package/packages/tooling-preference/package.json +1 -1
  146. package/packages/trajectory/package.json +1 -1
  147. package/packages/tui/dist/harness-footer.d.ts +2 -2
  148. package/packages/tui/dist/harness-footer.js +2 -2
  149. package/packages/tui/package.json +1 -1
  150. package/packages/tui/src/harness-footer.ts +2 -2
  151. package/packages/tui/test/harness-footer.test.ts +10 -10
  152. package/packages/two-phase-review/package.json +1 -1
  153. package/packages/types/package.json +1 -1
  154. package/packages/voice-api/package.json +1 -1
  155. package/packages/webmcp-tester/package.json +1 -1
  156. package/packages/wiki/package.json +1 -1
  157. package/packages/workflow-events/package.json +1 -1
  158. package/packages/workspace-scanner/package.json +1 -1
  159. package/packages/worktree/package.json +1 -1
  160. package/packages/write-review/package.json +1 -1
  161. package/scripts/convert-skills-to-okf.ts +4 -1
  162. package/scripts/r2-storage.ts +2 -1
  163. package/scripts/sync-callback-queue.ts +4 -3
  164. package/scripts/telegram-mobile-bot.ts +3 -2
  165. package/scripts/test-r2-telegram.ts +2 -1
  166. package/scripts/test-telegram-approval.ts +2 -1
  167. package/scripts/voice-api-example.ts +2 -1
package/AGENTS.md CHANGED
@@ -311,7 +311,7 @@ ssh user@host "kill old; sleep 2; uvicorn ... &"
311
311
  # CORRECT - SSH exits fast, process continues on server
312
312
  ssh -o BatchMode=yes user@host \
313
313
  "kill -9 917094 970065 2>/dev/null; sleep 1; \
314
- cd /home/frappe/amos-saas/backend/services/api && \
314
+ cd ~/amos-saas/backend/services/api && \
315
315
  nohup .venv/bin/uvicorn amos_api.main:app --host 0.0.0.0 --port 8100 \
316
316
  > /tmp/uvicorn.log 2>&1 & \
317
317
  echo 'UVICORN_STARTED'"
package/README.md CHANGED
@@ -1113,7 +1113,7 @@ Single server handles both Telegram and LINE webhooks on one domain.
1113
1113
  [Service]
1114
1114
  Type=simple
1115
1115
  User=frappe
1116
- ExecStart=/home/frappe/.bun/bin/bun run /home/frappe/unified-webhook-server.ts
1116
+ ExecStart=~/.bun/bin/bun run ~/unified-webhook-server.ts
1117
1117
  Restart=always
1118
1118
  RestartSec=5
1119
1119
 
@@ -9,14 +9,15 @@
9
9
  * Then copies to ~/.pi-harness-runtime/keys/ for easy access.
10
10
  */
11
11
 
12
+ import { homedir as osHomedir } from "node:os";
12
13
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
14
  import { join, dirname } from "node:path";
14
15
 
15
16
  /** pi.dev auth file location */
16
- const PI_AUTH_FILE = join(process.env.HOME || "/home/frappe", ".pi", "agent", "auth.json");
17
+ const PI_AUTH_FILE = join(process.env.HOME || osHomedir(), ".pi", "agent", "auth.json");
17
18
 
18
19
  /** Our keys directory */
19
- const KEYS_DIR = join(process.env.HOME || "/home/frappe", ".pi-harness-runtime", "keys");
20
+ const KEYS_DIR = join(process.env.HOME || osHomedir(), ".pi-harness-runtime", "keys");
20
21
 
21
22
  /** Key file mappings */
22
23
  const KEY_MAPPINGS: Record<string, string> = {
@@ -5,6 +5,7 @@
5
5
  * Users can configure which channels to use and toggle them via inline buttons.
6
6
  */
7
7
 
8
+ import { homedir as osHomedir } from "node:os";
8
9
  import * as https from "https";
9
10
 
10
11
  export interface NotificationConfig {
@@ -34,7 +35,7 @@ export interface NotificationButtons {
34
35
  style?: "primary" | "secondary" | "danger";
35
36
  }
36
37
 
37
- const DEFAULT_CONFIG_FILE = `${process.env.HOME || "/home/frappe"}/.pi-harness-runtime/notification-config.json`;
38
+ const DEFAULT_CONFIG_FILE = `${process.env.HOME || osHomedir()}/.pi-harness-runtime/notification-config.json`;
38
39
 
39
40
  /**
40
41
  * Load notification configuration
@@ -13,7 +13,7 @@ Study Herdr's reusable `SKILL.md` distribution model and adapt the idea to `pi-h
13
13
 
14
14
  ### Local source of truth to protect
15
15
 
16
- - Live skills: `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
16
+ - Live skills: `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
17
17
 
18
18
  ### Current runtime state
19
19
 
@@ -35,7 +35,7 @@ Study Herdr's reusable `SKILL.md` distribution model and adapt the idea to `pi-h
35
35
 
36
36
  The directory:
37
37
 
38
- `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
38
+ `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
39
39
 
40
40
  is continuously updated and acts as the operational knowledge base.
41
41
 
@@ -85,7 +85,7 @@ Use a **three-tier model**.
85
85
 
86
86
  Location:
87
87
 
88
- - `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
88
+ - `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
89
89
 
90
90
  Role:
91
91
 
@@ -6,7 +6,7 @@ Use this prompt with Minimax to implement the first safe slice of the MooCoding
6
6
 
7
7
  You are coding inside this repo:
8
8
 
9
- - `/home/frappe/pi-harness-runtime`
9
+ - `~/pi-harness-runtime`
10
10
 
11
11
  Read these planning notes first:
12
12
 
@@ -33,7 +33,7 @@ Do **not** implement sanitize or export in this task.
33
33
 
34
34
  The live private source of truth is:
35
35
 
36
- - `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
36
+ - `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
37
37
 
38
38
  This source changes often and may contain private or unsanitized content.
39
39
 
@@ -68,7 +68,7 @@ Suggested initial shape:
68
68
  ```json
69
69
  {
70
70
  "version": 1,
71
- "sourceRoot": "/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills",
71
+ "sourceRoot": "~/frappe-bench/.claude-plugins/moocoding-skills/skills",
72
72
  "skills": []
73
73
  }
74
74
  ```
@@ -10,7 +10,7 @@ This document turns the high-level Herdr adaptation plan into a concrete design
10
10
 
11
11
  This design assumes the live private skill corpus remains here:
12
12
 
13
- - `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
13
+ - `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
14
14
 
15
15
  That location remains the editable operational knowledge base.
16
16
 
@@ -60,7 +60,7 @@ Every public skill should have traceable provenance:
60
60
  Outside repo package boundary:
61
61
 
62
62
  ```text
63
- /home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/
63
+ ~/frappe-bench/.claude-plugins/moocoding-skills/skills/
64
64
  <skill-name>/
65
65
  SKILL.md
66
66
  ```
@@ -202,7 +202,7 @@ Example:
202
202
 
203
203
  ```json
204
204
  {
205
- "sourceRoot": "/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills",
205
+ "sourceRoot": "~/frappe-bench/.claude-plugins/moocoding-skills/skills",
206
206
  "skills": [
207
207
  {
208
208
  "sourceName": "bench-commands",
@@ -485,7 +485,7 @@ Document operator workflow
485
485
  This design does **not** recommend:
486
486
 
487
487
  - syncing the whole private corpus into public package directories
488
- - publishing directly from `/home/frappe/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
488
+ - publishing directly from `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
489
489
  - replacing the live MooCoding source with `pi-harness-runtime/skills/*`
490
490
  - auto-approving sanitized output without human review
491
491
 
@@ -148,14 +148,14 @@ describe("detectSsh", () => {
148
148
  });
149
149
 
150
150
  describe("isForegroundDaemonSsh", () => {
151
- // The exact command that caused the 1911.9s hang (2026-09-14)
151
+ // A synthetic stand-in for the foreground SSH command that used to hang
152
152
  const HANG_CMD =
153
- "echo 'manlucha' | ssh -tt frappe@100.101.238.114 \"cd /home/frappe/amos-saas/backend/services/worker && AMOS_API_URL=https://api.ezsysbot.com AMOS_WORKER_API_KEY=test /home/frappe/amos-saas/backend/.venv/bin/python -m amos_worker --video 2>&1 | head -10\"";
153
+ "echo 'synthetic-test-password' | ssh -tt worker@worker.example.com \"cd /srv/app/backend/services/worker && AMOS_API_URL=https://api.example.com AMOS_WORKER_API_KEY=test /srv/app/backend/.venv/bin/python -m amos_worker --video 2>&1 | head -10\"";
154
154
 
155
155
  const risky: Array<[string, string]> = [
156
156
  [
157
157
  HANG_CMD,
158
- "foreground ssh -tt running amos_worker poller (real 1911.9s hang)",
158
+ "foreground ssh -tt running amos_worker poller",
159
159
  ],
160
160
  [
161
161
  'ssh host "uvicorn main:app --host 0.0.0.0 --port 8100"',
@@ -0,0 +1,86 @@
1
+ /**
2
+ * SSH Hang Recovery — tests for the shell-safety helpers.
3
+ *
4
+ * The host, the user and the command all reach a shell. A value that is
5
+ * quoted cannot end the quote, and a value that does not look like a host
6
+ * is rejected before it is ever passed on.
7
+ */
8
+ import { describe, it } from "node:test";
9
+ import assert from "node:assert";
10
+ import { shellQuote, assertSafeSshTarget } from "./ssh-hang-recovery.js";
11
+
12
+ describe("shellQuote", () => {
13
+ it("wraps a plain value in single quotes", () => {
14
+ assert.strictEqual(shellQuote("pm2 status"), "'pm2 status'");
15
+ });
16
+
17
+ it("neutralizes a value that would close the quote", () => {
18
+ // The classic breakout: ' ; rm -rf / ; '
19
+ const payload = "'; rm -rf / ; '";
20
+ const quoted = shellQuote(payload);
21
+ // Every ' of the payload is closed, escaped and reopened, so the
22
+ // payload can never leave the quoted string.
23
+ assert.strictEqual(quoted, "''\\''; rm -rf / ; '\\'''");
24
+ // The quoted form has an even number of quotes: it parses as one
25
+ // literal string, whatever the payload was.
26
+ assert.strictEqual((quoted.match(/'/g) || []).length % 2, 0);
27
+ });
28
+
29
+ it("neutralizes a command substitution", () => {
30
+ const quoted = shellQuote("$(whoami)");
31
+ assert.strictEqual(quoted, "'$(whoami)'");
32
+ });
33
+
34
+ it("neutralizes a backtick substitution", () => {
35
+ const quoted = shellQuote("`id`");
36
+ assert.strictEqual(quoted, "'`id`'");
37
+ });
38
+
39
+ it("keeps a newline inside the quotes", () => {
40
+ const quoted = shellQuote("a\nb");
41
+ assert.strictEqual(quoted, "'a\nb'");
42
+ });
43
+ });
44
+
45
+ describe("assertSafeSshTarget", () => {
46
+ it("accepts a DNS name", () => {
47
+ assert.doesNotThrow(() => assertSafeSshTarget("example.com", "deploy"));
48
+ });
49
+
50
+ it("accepts an IPv4 address", () => {
51
+ assert.doesNotThrow(() => assertSafeSshTarget("203.0.113.10", "deploy"));
52
+ });
53
+
54
+ it("accepts a bracketed IPv6 address", () => {
55
+ assert.doesNotThrow(() => assertSafeSshTarget("[2001:db8::1]", "deploy"));
56
+ });
57
+
58
+ it("rejects a host that carries a command", () => {
59
+ assert.throws(
60
+ () => assertSafeSshTarget("example.com; rm -rf /", "deploy"),
61
+ /invalid ssh host/,
62
+ );
63
+ });
64
+
65
+ it("rejects a host that carries a substitution", () => {
66
+ assert.throws(
67
+ () => assertSafeSshTarget("$(whoami).example.com", "deploy"),
68
+ /invalid ssh host/,
69
+ );
70
+ });
71
+
72
+ it("rejects a host with a space", () => {
73
+ assert.throws(() => assertSafeSshTarget("a b", "deploy"), /invalid ssh host/);
74
+ });
75
+
76
+ it("rejects a user that carries a command", () => {
77
+ assert.throws(
78
+ () => assertSafeSshTarget("example.com", "root; rm -rf /"),
79
+ /invalid ssh user/,
80
+ );
81
+ });
82
+
83
+ it("rejects a user with a shell metacharacter", () => {
84
+ assert.throws(() => assertSafeSshTarget("example.com", "a$b"), /invalid ssh user/);
85
+ });
86
+ });
@@ -11,8 +11,8 @@
11
11
  * → Still failing? → Escalate to human with diagnostic
12
12
  */
13
13
 
14
- import { execSync, exec } from "child_process";
15
- import { setTimeout as setAsyncTimeout, clearTimeout } from "timers";
14
+ import { execSync, exec } from "node:child_process";
15
+ import { setTimeout as setAsyncTimeout, clearTimeout } from "node:timers";
16
16
 
17
17
  export interface SSHConfig {
18
18
  host: string;
@@ -33,11 +33,42 @@ export interface SSHResult {
33
33
  }
34
34
 
35
35
  const DEFAULT_TIMEOUT = 120_000; // 2 minutes
36
- const HANG_THRESHOLD_MS = 60_000; // 1 minute = likely hang
37
36
 
38
37
  /**
39
- * Sanitize input for shell commands - prevent command injection
38
+ * A host is either a DNS name, an IPv4 address or a bracketed IPv6 address.
39
+ * Anything else never reaches the shell.
40
40
  */
41
+ const SAFE_HOST = /^(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|\[[0-9A-Fa-f:.]+\])$/;
42
+
43
+ /** A login name on the remote machine. */
44
+ const SAFE_USER = /^[A-Za-z0-9._-]+$/;
45
+
46
+ /**
47
+ * Shell-quote a value for a POSIX shell.
48
+ *
49
+ * Every value that reaches the shell goes through here: a value that is
50
+ * wrapped in single quotes cannot end the quote and cannot start a new
51
+ * command, whatever it contains.
52
+ */
53
+ export function shellQuote(value: string): string {
54
+ return `'${value.replace(/'/g, `'\\''`)}'`;
55
+ }
56
+
57
+ /**
58
+ * Reject a host or user that is not a plain identifier.
59
+ *
60
+ * The host and the user are interpolated into a command line, so they are
61
+ * validated instead of quoted: a value that does not look like a host is
62
+ * never a host, and passing it on would only help an attacker.
63
+ */
64
+ export function assertSafeSshTarget(host: string, user: string): void {
65
+ if (!SAFE_HOST.test(host)) {
66
+ throw new Error(`Refusing to use an invalid ssh host: ${JSON.stringify(host)}`);
67
+ }
68
+ if (!SAFE_USER.test(user)) {
69
+ throw new Error(`Refusing to use an invalid ssh user: ${JSON.stringify(user)}`);
70
+ }
71
+ }
41
72
 
42
73
  /**
43
74
  * Execute SSH command with automatic hang recovery
@@ -54,7 +85,9 @@ export async function sshWithRecovery(
54
85
  strictHostKeyChecking = false,
55
86
  } = config;
56
87
 
57
- const sshBase = `ssh -o BatchMode=yes -o ConnectTimeout=30 -o StrictHostKeyChecking=${strictHostKeyChecking ? "yes" : "no"} ${user}@${host}`;
88
+ assertSafeSshTarget(host, user);
89
+
90
+ const sshBase = `ssh -o BatchMode=yes -o ConnectTimeout=30 -o StrictHostKeyChecking=${strictHostKeyChecking ? "yes" : "no"} ${shellQuote(user)}@${shellQuote(host)}`;
58
91
 
59
92
  let retries = 0;
60
93
  let hungProcessesKilled = false;
@@ -65,7 +98,7 @@ export async function sshWithRecovery(
65
98
  try {
66
99
  // Execute with timeout
67
100
  const result = await execAsync(
68
- `${sshBase} '${command.replace(/'/g, "'\\''")}'`,
101
+ `${sshBase} ${shellQuote(command)}`,
69
102
  timeout,
70
103
  );
71
104
 
@@ -129,17 +162,17 @@ export async function sshWithRecovery(
129
162
  async function killStuckProcesses(host: string, user: string): Promise<void> {
130
163
  const killCommands = [
131
164
  // Kill stuck SSH sessions
132
- `pkill -f "ssh.*${host}" || true`,
165
+ `pkill -f ${shellQuote(`ssh.*${host}`)} || true`,
133
166
  // Kill stuck PM2 commands
134
- `pkill -f "pm2.*amos-saas" || true`,
167
+ `pkill -f ${shellQuote("pm2.*")} || true`,
135
168
  // Kill stuck sudo commands
136
- `pkill -f "sudo.*pm2" || true`,
169
+ `pkill -f ${shellQuote("sudo.*pm2")} || true`,
137
170
  ];
138
171
 
139
172
  for (const cmd of killCommands) {
140
173
  try {
141
174
  execSync(
142
- `ssh -o BatchMode=yes -o ConnectTimeout=10 ${user}@${host} '${cmd}'`,
175
+ `ssh -o BatchMode=yes -o ConnectTimeout=10 ${shellQuote(user)}@${shellQuote(host)} ${shellQuote(cmd)}`,
143
176
  { timeout: 10_000, encoding: "utf-8" },
144
177
  );
145
178
  } catch {
@@ -154,7 +187,7 @@ async function killStuckProcesses(host: string, user: string): Promise<void> {
154
187
  function killLocalSSHProcesses(host: string): void {
155
188
  try {
156
189
  // Kill any local SSH processes connecting to this host
157
- execSync(`pkill -f "ssh.*${host}" || true`, { timeout: 5000 });
190
+ execSync(`pkill -f ${shellQuote(`ssh.*${host}`)} || true`, { timeout: 5000 });
158
191
  } catch {
159
192
  // Ignore
160
193
  }
@@ -168,8 +201,9 @@ export async function pingServer(
168
201
  user: string = "frappe",
169
202
  ): Promise<boolean> {
170
203
  try {
204
+ assertSafeSshTarget(host, user);
171
205
  execSync(
172
- `ssh -o BatchMode=yes -o ConnectTimeout=5 ${user}@${host} 'echo ok'`,
206
+ `ssh -o BatchMode=yes -o ConnectTimeout=5 ${shellQuote(user)}@${shellQuote(host)} ${shellQuote("echo ok")}`,
173
207
  { timeout: 10_000 },
174
208
  );
175
209
  return true;
@@ -186,7 +220,7 @@ export async function getPM2Status(
186
220
  pm2Path?: string,
187
221
  ): Promise<SSHResult> {
188
222
  const pm2Cmd = pm2Path || "pm2";
189
- const command = `PM2_HOME=/home/frappe/.pm2 ${pm2Cmd} status`;
223
+ const command = `PM2_HOME=~/.pm2 ${pm2Cmd} status`;
190
224
 
191
225
  return sshWithRecovery(
192
226
  {
@@ -205,7 +239,7 @@ export async function pm2Restart(
205
239
  host: string,
206
240
  app: string,
207
241
  ): Promise<SSHResult> {
208
- const command = `PM2_HOME=/home/frappe/.pm2 pm2 restart ${app}`;
242
+ const command = `PM2_HOME=~/.pm2 pm2 restart ${app}`;
209
243
 
210
244
  return sshWithRecovery(
211
245
  {
package/index.ts CHANGED
@@ -672,8 +672,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
672
672
  addHarnessMessage("Jev", "initializing", "wait");
673
673
  try {
674
674
  // Check if Jev API key is available (env var or keys file)
675
- const homedir = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
676
- const keysDir = `${homedir}/.pi-harness-runtime/keys`;
675
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
676
+ const keysDir = `${home}/.pi-harness-runtime/keys`;
677
677
  const { existsSync, mkdirSync } = await import("fs");
678
678
 
679
679
  // Auto-create directory if doesn't exist
@@ -1997,7 +1997,7 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1997
1997
  const NotificationCenter = notifModule.NotificationCenter;
1998
1998
  const createConversationBridge = notifModule.createConversationBridge;
1999
1999
  const { readFileSync, existsSync } = await import("node:fs");
2000
- const home = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
2000
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
2001
2001
  const keysDir = `${home}/.pi-harness-runtime/keys`;
2002
2002
 
2003
2003
  const botTokenPath = `${keysDir}/telegram-bot-token.txt`;
@@ -2859,7 +2859,7 @@ async function initHoncho(pi: ExtensionAPI): Promise<void> {
2859
2859
 
2860
2860
  // Check for API key
2861
2861
  const { readFileSync, existsSync } = await import("node:fs");
2862
- const home = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
2862
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
2863
2863
  const keyPath = `${home}/.pi-harness-runtime/keys/honcho-api-key.txt`;
2864
2864
 
2865
2865
  if (!existsSync(keyPath)) {
@@ -2930,7 +2930,7 @@ function initLangChain(pi: {
2930
2930
  const { dirname } = require("node:path");
2931
2931
  const readline = require("node:readline");
2932
2932
 
2933
- const home = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
2933
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
2934
2934
  const keysDir = `${home}/.pi-harness-runtime/keys`;
2935
2935
  const modelsStorePath = `${home}/.pi/agent/models-store.json`;
2936
2936
  const roleConfigPath = `${keysDir}/langchain-role.json`;
@@ -3973,8 +3973,8 @@ function isOutputLimitResumePromptMessage(message: {
3973
3973
  // → pi-harness polls GET /api/pending-commands
3974
3974
  // → pi-harness injects "resume" via steer
3975
3975
  function initJobPipeWatcher(pi: ExtensionAPI): void {
3976
- const homedir = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
3977
- const keysDir = join(homedir, ".pi-harness-runtime", "keys");
3976
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
3977
+ const keysDir = join(home, ".pi-harness-runtime", "keys");
3978
3978
 
3979
3979
  // Get webhook server URL and API key from config
3980
3980
  const getWebhookConfig = () => {
@@ -4101,8 +4101,8 @@ function initJobPipeWatcher(pi: ExtensionAPI): void {
4101
4101
  // → pi-harness polls this file
4102
4102
  // → pi-harness injects prompt via steer
4103
4103
  function initLineVoiceQueue(pi: ExtensionAPI): void {
4104
- const homedir = process.env.HOME || process.env.USERPROFILE || "/home/frappe";
4105
- const queueFile = join(homedir, ".pi-harness-runtime", "line-voice-queue.jsonl");
4104
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
4105
+ const queueFile = join(home, ".pi-harness-runtime", "line-voice-queue.jsonl");
4106
4106
 
4107
4107
  const processLineVoiceQueue = async () => {
4108
4108
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cli-plugin-sdk",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "CLI Plugin SDK for pi-harness — install, list, remove, update, invoke plugins",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard-plugin",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-generation",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Code generation pipeline for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-review",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Automated code review engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/codex-adapter",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "OpenAI Codex provider adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/codex-watcher",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Watches Codex CLI sessions and emits visible messages for PING-PONG monitoring",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/config-capture",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Auto-capture API-fetched configuration values to AGENTS.md",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/connection-runtime",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "description": "Connection management with jittered exponential backoff for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-compiler",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Bounded context selection engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-discovery",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/context-manager",
3
- "version": "1.1.425",
3
+ "version": "1.1.426",
4
4
  "description": "Context Window Manager — prevents model context overflow via compaction and resume prompts (RFC-0010)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",