@pushpalsdev/cli 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pushpals-cli.js +291 -44
- package/package.json +1 -1
- package/runtime/configs/backend.toml +1 -1
- package/runtime/configs/default.toml +1 -1
- package/runtime/sandbox/apps/workerpals/.python-version +1 -0
- package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
- package/runtime/sandbox/apps/workerpals/package.json +25 -0
- package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
- package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +119 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
- package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
- package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
- package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
- package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
- package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
- package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
- package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
- package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
- package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
- package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
- package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
- package/runtime/sandbox/bun.lock +2591 -0
- package/runtime/sandbox/configs/backend.toml +79 -0
- package/runtime/sandbox/configs/default.toml +260 -0
- package/runtime/sandbox/configs/dev.toml +2 -0
- package/runtime/sandbox/configs/local.example.toml +129 -0
- package/runtime/sandbox/package.json +65 -0
- package/runtime/sandbox/packages/protocol/README.md +168 -0
- package/runtime/sandbox/packages/protocol/package.json +37 -0
- package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
- package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
- package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
- package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
- package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
- package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
- package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
- package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
- package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
- package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
- package/runtime/sandbox/packages/shared/package.json +19 -0
- package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
- package/runtime/sandbox/packages/shared/src/client_preflight.ts +286 -0
- package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
- package/runtime/sandbox/packages/shared/src/config.ts +2180 -0
- package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
- package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
- package/runtime/sandbox/packages/shared/src/index.ts +101 -0
- package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
- package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +314 -0
- package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
- package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
- package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
- package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
- package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
- package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
- package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
- package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
- package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
- package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
- package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Docker Job Runner - Standalone job execution daemon inside Docker
|
|
4
|
+
*
|
|
5
|
+
* This script runs inside a Docker container and executes a single job.
|
|
6
|
+
* It's designed to be the entrypoint for sandboxed job execution.
|
|
7
|
+
*
|
|
8
|
+
* Usage (inside container):
|
|
9
|
+
* bun run job_runner.ts <base64-encoded-job-spec>
|
|
10
|
+
*
|
|
11
|
+
* The job spec is base64-encoded JSON: { jobId, taskId, kind, params, workerId }
|
|
12
|
+
*
|
|
13
|
+
* Output:
|
|
14
|
+
* stderr → JSON log lines: {"stream":"stdout|stderr","line":"..."}
|
|
15
|
+
* stdout → Result with sentinel: ___RESULT___ {"ok":true,...,"commit":{...}}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { executeJob, shouldCommit, createJobCommit } from "./execute_job.js";
|
|
19
|
+
import { loadPushPalsConfig } from "shared";
|
|
20
|
+
import { writeFileSync } from "fs";
|
|
21
|
+
|
|
22
|
+
const CONFIG = loadPushPalsConfig();
|
|
23
|
+
|
|
24
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface JobSpec {
|
|
27
|
+
jobId: string;
|
|
28
|
+
taskId: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
params: Record<string, unknown>;
|
|
31
|
+
workerId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface JobResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
summary: string;
|
|
37
|
+
stdout?: string;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
exitCode?: number;
|
|
40
|
+
commit?: {
|
|
41
|
+
branch: string;
|
|
42
|
+
sha: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Logging helpers ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function log(stream: "stdout" | "stderr", line: string): void {
|
|
49
|
+
const json = JSON.stringify({ stream, line });
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.error(json);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Git credentials setup ──────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function setupGitCredentials(): void {
|
|
57
|
+
const token = CONFIG.gitToken ?? process.env.GIT_TOKEN ?? null;
|
|
58
|
+
if (!token) return;
|
|
59
|
+
try {
|
|
60
|
+
// Use a credential helper script and avoid remote URL rewriting with embedded secrets.
|
|
61
|
+
// This keeps push auth stable and prevents token leakage in git stderr output.
|
|
62
|
+
const helperScript = `#!/bin/sh
|
|
63
|
+
echo "username=x-access-token"
|
|
64
|
+
echo "password=${token}"
|
|
65
|
+
`;
|
|
66
|
+
const helperPath = "/tmp/git-credential-helper";
|
|
67
|
+
writeFileSync(helperPath, helperScript, { mode: 0o755 });
|
|
68
|
+
|
|
69
|
+
// Remove any legacy URL rewrite rules that may have embedded token credentials.
|
|
70
|
+
const urlRules = Bun.spawnSync(["git", "config", "--global", "--get-regexp", "^url\\..*\\.insteadOf$"], {
|
|
71
|
+
stdout: "pipe",
|
|
72
|
+
stderr: "pipe",
|
|
73
|
+
});
|
|
74
|
+
if (urlRules.exitCode === 0) {
|
|
75
|
+
const lines = String(urlRules.stdout ?? "")
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const key = line.split(/\s+/, 1)[0] ?? "";
|
|
81
|
+
if (!key) continue;
|
|
82
|
+
const lower = key.toLowerCase();
|
|
83
|
+
if (!lower.startsWith("url.")) continue;
|
|
84
|
+
if (!lower.endsWith(".insteadof")) continue;
|
|
85
|
+
if (!lower.includes("oauth2") && !lower.includes("%3a//")) continue;
|
|
86
|
+
Bun.spawnSync(["git", "config", "--global", "--unset-all", key], {
|
|
87
|
+
stdout: "pipe",
|
|
88
|
+
stderr: "pipe",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Bun.spawnSync(["git", "config", "--global", "credential.helper", helperPath]);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
log("stderr", `Failed to setup git credentials: ${err}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
async function main(): Promise<void> {
|
|
102
|
+
const args = process.argv.slice(2);
|
|
103
|
+
const base64Spec = args[0];
|
|
104
|
+
|
|
105
|
+
if (!base64Spec) {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.error("Usage: bun run job_runner.ts <base64-encoded-job-spec>");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Decode base64 job spec
|
|
112
|
+
let spec: JobSpec;
|
|
113
|
+
try {
|
|
114
|
+
const json = Buffer.from(base64Spec, "base64").toString("utf-8");
|
|
115
|
+
spec = JSON.parse(json);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.error(`Failed to decode job spec: ${err}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
log("stdout", `[JobRunner] Starting job ${spec.jobId} (${spec.kind})`);
|
|
122
|
+
// Setup git credentials for pushing
|
|
123
|
+
setupGitCredentials();
|
|
124
|
+
// Execute inside the mounted job worktree (docker -w), not the baked image copy.
|
|
125
|
+
const jobRepo = process.cwd();
|
|
126
|
+
const result = await executeJob(
|
|
127
|
+
spec.kind,
|
|
128
|
+
spec.params,
|
|
129
|
+
jobRepo,
|
|
130
|
+
(stream, line) => {
|
|
131
|
+
log(stream, line);
|
|
132
|
+
},
|
|
133
|
+
CONFIG,
|
|
134
|
+
);
|
|
135
|
+
// Build result object
|
|
136
|
+
const jobResult: JobResult = {
|
|
137
|
+
ok: result.ok,
|
|
138
|
+
summary: result.summary,
|
|
139
|
+
stdout: result.stdout,
|
|
140
|
+
stderr: result.stderr,
|
|
141
|
+
exitCode: result.exitCode,
|
|
142
|
+
};
|
|
143
|
+
// Create commit for file-modifying jobs
|
|
144
|
+
if (result.ok && shouldCommit(spec.kind, CONFIG)) {
|
|
145
|
+
log("stdout", `[JobRunner] Job modified files, creating commit...`);
|
|
146
|
+
const commitResult = await createJobCommit(
|
|
147
|
+
jobRepo,
|
|
148
|
+
spec.workerId,
|
|
149
|
+
{
|
|
150
|
+
id: spec.jobId,
|
|
151
|
+
taskId: spec.taskId,
|
|
152
|
+
kind: spec.kind,
|
|
153
|
+
params: spec.params,
|
|
154
|
+
context: "docker",
|
|
155
|
+
},
|
|
156
|
+
CONFIG,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (commitResult.ok && commitResult.sha && commitResult.branch) {
|
|
160
|
+
jobResult.commit = {
|
|
161
|
+
branch: commitResult.branch!,
|
|
162
|
+
sha: commitResult.sha,
|
|
163
|
+
};
|
|
164
|
+
if (commitResult.sha === "no-changes") {
|
|
165
|
+
log("stdout", `[JobRunner] No changes to commit for ${spec.jobId}`);
|
|
166
|
+
} else {
|
|
167
|
+
log("stdout", `[JobRunner] Created commit ${commitResult.sha} on ${commitResult.branch}`);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const commitError =
|
|
171
|
+
commitResult.error ??
|
|
172
|
+
`Commit metadata missing for ${spec.kind} (${spec.jobId}) while running in Docker mode`;
|
|
173
|
+
jobResult.ok = false;
|
|
174
|
+
jobResult.summary = `Failed to create commit for ${spec.kind}`;
|
|
175
|
+
jobResult.stderr = [jobResult.stderr, commitError].filter(Boolean).join("\n");
|
|
176
|
+
jobResult.exitCode = jobResult.exitCode && jobResult.exitCode !== 0 ? jobResult.exitCode : 1;
|
|
177
|
+
log("stderr", `[JobRunner] Failed to create commit: ${commitError}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Output result with sentinel
|
|
182
|
+
const resultJson = JSON.stringify(jobResult);
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.log(`___RESULT___ ${resultJson}`);
|
|
185
|
+
|
|
186
|
+
// Exit with appropriate code
|
|
187
|
+
process.exit(jobResult.exitCode ?? (jobResult.ok ? 0 : 1));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch((err) => {
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.error(`[JobRunner] Fatal error: ${err}`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Persistent ShellManager for multi-worker shell sessions
|
|
2
|
+
// Milestone 1: Correctness, lease management, command framing, audit logging
|
|
3
|
+
|
|
4
|
+
import { spawn } from "bun";
|
|
5
|
+
import Database from "bun:sqlite";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { ContextManager } from "./context_manager";
|
|
8
|
+
|
|
9
|
+
const SESSION_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
10
|
+
const LEASE_DURATION_MS = 60 * 1000; // 1 min
|
|
11
|
+
|
|
12
|
+
export interface ShellCommand {
|
|
13
|
+
cmdId: string;
|
|
14
|
+
command: string;
|
|
15
|
+
status: "pending" | "running" | "completed" | "crashed" | "killed";
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
stdout?: string;
|
|
18
|
+
stderr?: string;
|
|
19
|
+
startedAt?: number;
|
|
20
|
+
completedAt?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ShellManager {
|
|
24
|
+
private db: Database;
|
|
25
|
+
private workerId: string;
|
|
26
|
+
private sessions = new Map<string, ShellSessionRuntime>();
|
|
27
|
+
private contextMgr: ContextManager;
|
|
28
|
+
|
|
29
|
+
constructor(db: Database, workerId: string) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.workerId = workerId;
|
|
32
|
+
this.contextMgr = new ContextManager(db);
|
|
33
|
+
this.ensureTables();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ensureTables() {
|
|
37
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS shell_sessions (
|
|
38
|
+
session_id TEXT PRIMARY KEY,
|
|
39
|
+
worker_id TEXT,
|
|
40
|
+
lease_expiry INTEGER,
|
|
41
|
+
last_used_at INTEGER,
|
|
42
|
+
last_known_cwd TEXT,
|
|
43
|
+
status TEXT
|
|
44
|
+
)`);
|
|
45
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS shell_commands (
|
|
46
|
+
cmd_id TEXT PRIMARY KEY,
|
|
47
|
+
session_id TEXT,
|
|
48
|
+
command TEXT,
|
|
49
|
+
status TEXT,
|
|
50
|
+
exit_code INTEGER,
|
|
51
|
+
stdout TEXT,
|
|
52
|
+
stderr TEXT,
|
|
53
|
+
started_at INTEGER,
|
|
54
|
+
completed_at INTEGER
|
|
55
|
+
)`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
acquireLease(sessionId: string): boolean {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const expiry = now + LEASE_DURATION_MS;
|
|
61
|
+
// Try to acquire lease
|
|
62
|
+
this.db.run(
|
|
63
|
+
`INSERT OR IGNORE INTO shell_sessions (session_id, worker_id, lease_expiry, last_used_at, status) VALUES (?, ?, ?, ?, ?)`,
|
|
64
|
+
[sessionId, this.workerId, expiry, now, "active"],
|
|
65
|
+
);
|
|
66
|
+
const row = this.db
|
|
67
|
+
.query(`SELECT worker_id, lease_expiry FROM shell_sessions WHERE session_id = ?`)
|
|
68
|
+
.get(sessionId) as { worker_id: string; lease_expiry: number } | null;
|
|
69
|
+
if (row && (row.worker_id === this.workerId || row.lease_expiry < now)) {
|
|
70
|
+
this.db.run(
|
|
71
|
+
`UPDATE shell_sessions SET worker_id = ?, lease_expiry = ?, last_used_at = ?, status = ? WHERE session_id = ?`,
|
|
72
|
+
[this.workerId, expiry, now, "active", sessionId],
|
|
73
|
+
);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getOrCreate(sessionId: string): ShellSessionRuntime | null {
|
|
80
|
+
if (!this.acquireLease(sessionId)) return null;
|
|
81
|
+
let runtime = this.sessions.get(sessionId);
|
|
82
|
+
if (!runtime) {
|
|
83
|
+
// Restore last_known_cwd from context
|
|
84
|
+
const cwd = this.contextMgr.get(sessionId, "last_known_cwd") || process.cwd();
|
|
85
|
+
runtime = new ShellSessionRuntime(sessionId, this.workerId, this.db, this.contextMgr, cwd);
|
|
86
|
+
this.sessions.set(sessionId, runtime);
|
|
87
|
+
}
|
|
88
|
+
return runtime;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cleanupIdleSessions() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
for (const [sessionId, runtime] of this.sessions) {
|
|
94
|
+
if (now - runtime.lastUsedAt > SESSION_TTL_MS) {
|
|
95
|
+
runtime.terminate();
|
|
96
|
+
this.sessions.delete(sessionId);
|
|
97
|
+
this.db.run(`UPDATE shell_sessions SET status = ? WHERE session_id = ?`, [
|
|
98
|
+
"stopped",
|
|
99
|
+
sessionId,
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
startContextRefresh(intervalMs = 10000) {
|
|
106
|
+
setInterval(() => {
|
|
107
|
+
for (const sessionId of this.sessions.keys()) {
|
|
108
|
+
// Refresh context for this session
|
|
109
|
+
const context = this.contextMgr.getAll(sessionId);
|
|
110
|
+
// Feed context to agent if handler exists
|
|
111
|
+
const session = this.sessions.get(sessionId);
|
|
112
|
+
if (session && typeof session.onContextRefresh === "function") {
|
|
113
|
+
session.onContextRefresh(context);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, intervalMs);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class ShellSessionRuntime {
|
|
121
|
+
private sessionId: string;
|
|
122
|
+
private workerId: string;
|
|
123
|
+
private db: Database;
|
|
124
|
+
private shellProc: ReturnType<typeof spawn> | null = null;
|
|
125
|
+
private commandQueue: ShellCommand[] = [];
|
|
126
|
+
public lastUsedAt: number = Date.now();
|
|
127
|
+
public cwd: string = process.cwd();
|
|
128
|
+
private running: boolean = false;
|
|
129
|
+
private contextMgr: ContextManager;
|
|
130
|
+
public onContextRefresh?: (context: Record<string, string>) => void;
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
sessionId: string,
|
|
134
|
+
workerId: string,
|
|
135
|
+
db: Database,
|
|
136
|
+
contextMgr: ContextManager,
|
|
137
|
+
cwd: string,
|
|
138
|
+
) {
|
|
139
|
+
this.sessionId = sessionId;
|
|
140
|
+
this.workerId = workerId;
|
|
141
|
+
this.db = db;
|
|
142
|
+
this.contextMgr = contextMgr;
|
|
143
|
+
this.cwd = cwd;
|
|
144
|
+
this.spawnShell();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
spawnShell() {
|
|
148
|
+
// Spawn shell process (PowerShell or bash)
|
|
149
|
+
const isWindows = process.platform === "win32";
|
|
150
|
+
const shell = isWindows ? "powershell.exe" : "bash";
|
|
151
|
+
this.shellProc = spawn([shell], {
|
|
152
|
+
cwd: this.cwd,
|
|
153
|
+
stdin: "pipe",
|
|
154
|
+
stdout: "pipe",
|
|
155
|
+
stderr: "pipe",
|
|
156
|
+
});
|
|
157
|
+
// Attach parser, etc.
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
terminate() {
|
|
161
|
+
if (this.shellProc) {
|
|
162
|
+
this.shellProc.kill();
|
|
163
|
+
this.shellProc = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
enqueueCommand(command: string) {
|
|
168
|
+
const cmdId = randomUUID();
|
|
169
|
+
const shellCmd: ShellCommand = {
|
|
170
|
+
cmdId,
|
|
171
|
+
command,
|
|
172
|
+
status: "pending",
|
|
173
|
+
startedAt: Date.now(),
|
|
174
|
+
};
|
|
175
|
+
this.commandQueue.push(shellCmd);
|
|
176
|
+
const startedAt = shellCmd.startedAt ?? Date.now();
|
|
177
|
+
this.db.run(
|
|
178
|
+
`INSERT INTO shell_commands (cmd_id, session_id, command, status, started_at) VALUES (?, ?, ?, ?, ?)`,
|
|
179
|
+
[cmdId, this.sessionId, command, "pending", startedAt],
|
|
180
|
+
);
|
|
181
|
+
this.processQueue();
|
|
182
|
+
return cmdId;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async processQueue() {
|
|
186
|
+
if (this.running || this.commandQueue.length === 0) return;
|
|
187
|
+
this.running = true;
|
|
188
|
+
const cmd = this.commandQueue.shift();
|
|
189
|
+
if (!cmd || !this.shellProc) {
|
|
190
|
+
this.running = false;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Framing: BEGIN/EXIT/CWD/END
|
|
194
|
+
const framed = `echo BEGIN ${cmd.cmdId}\n${cmd.command}\necho EXIT ${cmd.cmdId} $?\necho CWD ${cmd.cmdId} $(pwd)\necho END ${cmd.cmdId}`;
|
|
195
|
+
const stdin = this.shellProc.stdin;
|
|
196
|
+
if (!stdin || typeof stdin === "number" || typeof stdin.write !== "function") {
|
|
197
|
+
this.running = false;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
stdin.write(framed + "\n");
|
|
201
|
+
// TODO: parse output, update db, handle exit/cwd
|
|
202
|
+
// TODO: renew lease, update last_used_at
|
|
203
|
+
// On command completion, update last_known_cwd in context
|
|
204
|
+
if (this.cwd) {
|
|
205
|
+
this.contextMgr.set(this.sessionId, "last_known_cwd", this.cwd);
|
|
206
|
+
}
|
|
207
|
+
this.running = false;
|
|
208
|
+
this.processQueue();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DEFAULT_OPENHANDS_TIMEOUT_MS = 1_800_000;
|
|
2
|
+
export const DEFAULT_DOCKER_TIMEOUT_MS = 1_860_000;
|
|
3
|
+
|
|
4
|
+
export function parseOpenHandsTimeoutMs(raw: string | undefined): number {
|
|
5
|
+
const parsed = parseInt(raw ?? "", 10);
|
|
6
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_OPENHANDS_TIMEOUT_MS;
|
|
7
|
+
return Math.max(10_000, parsed);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseDockerTimeoutMs(raw: string | undefined): number {
|
|
11
|
+
const parsed = parseInt(raw ?? "", 10);
|
|
12
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DOCKER_TIMEOUT_MS;
|
|
13
|
+
return Math.max(10_000, parsed);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function computeTimeoutWarningWindow(timeoutMs: number): {
|
|
17
|
+
leadMs: number;
|
|
18
|
+
delayMs: number;
|
|
19
|
+
} {
|
|
20
|
+
const normalized = Math.max(10_000, Math.floor(timeoutMs));
|
|
21
|
+
const leadMs = Math.min(60_000, Math.max(10_000, normalized - 5_000));
|
|
22
|
+
const delayMs = Math.max(1_000, normalized - leadMs);
|
|
23
|
+
return { leadMs, delayMs };
|
|
24
|
+
}
|