pi-harness-runtime 1.1.424 → 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.
- package/AGENTS.md +1 -1
- package/README.md +2 -2
- package/harness/e2e/api-key-resolver.ts +3 -2
- package/harness/notification-center.ts +2 -1
- package/harness/skills-plan/2026-07-12-herdr-moo-skills-adaptation-plan.md +9 -8
- package/harness/skills-plan/2026-07-12-minimax-prompt-step1-sync.md +3 -3
- package/harness/skills-plan/2026-07-12-public-skill-export-and-sync-design.md +4 -4
- package/harness/ssh-detach-interceptor.test.ts +3 -3
- package/harness/ssh-hang-recovery.test.ts +86 -0
- package/harness/ssh-hang-recovery.ts +48 -14
- package/index.ts +9 -9
- package/package.json +5 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/delegation-store.js +2 -1
- package/packages/contracts/dist/workspace-env-format.d.ts +2 -2
- package/packages/contracts/dist/workspace-env-format.js +2 -2
- package/packages/contracts/dist/workspace-env.js +2 -1
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/delegation-store.ts +2 -1
- package/packages/contracts/src/workspace-env-format.ts +2 -2
- package/packages/contracts/src/workspace-env.ts +2 -1
- package/packages/contracts/test/workspace-env.test.ts +6 -6
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/README.md +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/delegation-harness.d.ts.map +1 -1
- package/packages/jev-judge/dist/delegation-harness.js +2 -1
- package/packages/jev-judge/dist/delegation-harness.js.map +1 -1
- package/packages/jev-judge/dist/index.d.ts.map +1 -1
- package/packages/jev-judge/dist/index.js +2 -1
- package/packages/jev-judge/dist/index.js.map +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/delegation-harness.ts +2 -1
- package/packages/jev-judge/src/index.ts +2 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/logging/test/classification.test.ts +2 -2
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +2 -2
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +2 -1
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/telegram-adapter.ts +2 -2
- package/packages/notification/src/notification-center.ts +2 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
- package/packages/paperclip-integration/dist/index.js +3 -2
- package/packages/paperclip-integration/dist/index.js.map +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/paperclip-integration/src/index.ts +4 -2
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/relay/test/relay-e2e.test.ts +4 -4
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +2 -1
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/loader.ts +2 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/dist/harness-footer.d.ts +2 -2
- package/packages/tui/dist/harness-footer.js +2 -2
- package/packages/tui/package.json +1 -1
- package/packages/tui/src/harness-footer.ts +2 -2
- package/packages/tui/test/harness-footer.test.ts +10 -10
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/convert-skills-to-okf.ts +4 -1
- package/scripts/r2-storage.ts +2 -1
- package/scripts/sync-callback-queue.ts +4 -3
- package/scripts/telegram-mobile-bot.ts +3 -2
- package/scripts/test-r2-telegram.ts +2 -1
- package/scripts/test-telegram-approval.ts +2 -1
- package/scripts/verify-package-contents.ts +57 -1
- package/scripts/voice-api-example.ts +2 -1
- package/skills/harness-runtime/SKILL.md +300 -0
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
|
|
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
|
@@ -37,7 +37,7 @@ pi install npm:@juicesharp/rpiv-todo # Task tracking with bd sync
|
|
|
37
37
|
|
|
38
38
|
The `pi-subagents` extension enables delegation assessment and parallel task execution for large refactoring work (10+ files, 50+ call sites).
|
|
39
39
|
|
|
40
|
-
The `reverse-engineer-anything` skill (via `rea-agents`) enables reverse engineering of binaries, packages, firmware, and browser targets.
|
|
40
|
+
The `reverse-engineer-anything` skill (via `rea-agents`) enables reverse engineering of binaries, packages, firmware, and browser targets. The package ships one skill of its own, `skills/harness-runtime/`, which tells your agent what the harness is. Every other skill is loaded from your own skill store (`~/.pi-harness-runtime/skills`) and from your own Qdrant collection.
|
|
41
41
|
|
|
42
42
|
Requires Pi v0.37.3+.
|
|
43
43
|
|
|
@@ -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
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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,14 +13,15 @@ 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:
|
|
16
|
+
- Live skills: `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
|
|
17
17
|
|
|
18
18
|
### Current runtime state
|
|
19
19
|
|
|
20
|
-
- `pi-harness-runtime` ships
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
20
|
+
- `pi-harness-runtime` ships exactly one skill of its own:
|
|
21
|
+
- `package.json` -> `pi.skills = ["./skills"]`
|
|
22
|
+
- `skills/harness-runtime/SKILL.md` - the skill of the package itself
|
|
23
|
+
- every other skill payload lives in the user's own Qdrant collection and
|
|
24
|
+
in the installation-local skill store, never in the published package
|
|
24
25
|
- `packages/skills` is the runtime implementation: it discovers, sanitizes,
|
|
25
26
|
embeds and queries skills.
|
|
26
27
|
- The scanner still reads the `skills/` and `.agents/skills/` directories of
|
|
@@ -34,7 +35,7 @@ Study Herdr's reusable `SKILL.md` distribution model and adapt the idea to `pi-h
|
|
|
34
35
|
|
|
35
36
|
The directory:
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
`~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
|
|
38
39
|
|
|
39
40
|
is continuously updated and acts as the operational knowledge base.
|
|
40
41
|
|
|
@@ -84,7 +85,7 @@ Use a **three-tier model**.
|
|
|
84
85
|
|
|
85
86
|
Location:
|
|
86
87
|
|
|
87
|
-
-
|
|
88
|
+
- `~/frappe-bench/.claude-plugins/moocoding-skills/skills/*`
|
|
88
89
|
|
|
89
90
|
Role:
|
|
90
91
|
|
|
@@ -195,7 +196,7 @@ Success criteria:
|
|
|
195
196
|
|
|
196
197
|
Once a sanitized skill exists inside `pi-harness-runtime/skills/<name>/SKILL.md`:
|
|
197
198
|
|
|
198
|
-
- expose it through the
|
|
199
|
+
- expose it through the existing `pi.skills` package field
|
|
199
200
|
- document Herdr-style installation ergonomics for the public subset
|
|
200
201
|
|
|
201
202
|
This gives the benefit of Herdr's distribution model without risking raw private content leakage.
|
|
@@ -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
|
-
-
|
|
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
|
-
-
|
|
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": "
|
|
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
|
-
-
|
|
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
|
-
|
|
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": "
|
|
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
|
|
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
|
-
//
|
|
151
|
+
// A synthetic stand-in for the foreground SSH command that used to hang
|
|
152
152
|
const HANG_CMD =
|
|
153
|
-
"echo '
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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}
|
|
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
|
|
165
|
+
`pkill -f ${shellQuote(`ssh.*${host}`)} || true`,
|
|
133
166
|
// Kill stuck PM2 commands
|
|
134
|
-
`pkill -f "pm2.*
|
|
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}
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
676
|
-
const keysDir = `${
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|
|
3977
|
-
const keysDir = join(
|
|
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
|
|
4105
|
-
const queueFile = join(
|
|
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.
|
|
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": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"prompts/**/*",
|
|
42
42
|
"scripts/**/*",
|
|
43
43
|
"plugins/**/*",
|
|
44
|
+
"skills/harness-runtime/**/*",
|
|
44
45
|
"package.json",
|
|
45
46
|
"README.md",
|
|
46
47
|
"AGENTS.md",
|
|
@@ -74,6 +75,9 @@
|
|
|
74
75
|
],
|
|
75
76
|
"prompts": [
|
|
76
77
|
"./prompts"
|
|
78
|
+
],
|
|
79
|
+
"skills": [
|
|
80
|
+
"./skills"
|
|
77
81
|
]
|
|
78
82
|
},
|
|
79
83
|
"peerDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
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",
|