@galaxy-stack/ai-coder-core 0.3.1 → 0.3.3

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 (35) hide show
  1. package/CHANGELOG.d/2026-09-30-baseline-review-bo-qua-thay-doi-generated.md +23 -0
  2. package/CHANGELOG.d/2026-09-30-galaxy-credentials.md +15 -0
  3. package/CHANGELOG.d/2026-09-30-shell-persistent-giong-dsh.md +25 -0
  4. package/CHANGELOG.d/2026-09-30-timeout-do-agent-tu-quyet.md +23 -0
  5. package/CHANGELOG.d/2026-09-30-timeout-giong-dsh.md +26 -0
  6. package/CHANGELOG.md +19 -0
  7. package/dist/adapters/node/config/galaxy-credentials.d.ts +58 -0
  8. package/dist/adapters/node/config/galaxy-credentials.d.ts.map +1 -0
  9. package/dist/adapters/node/config/galaxy-credentials.js +216 -0
  10. package/dist/adapters/node/config/galaxy-credentials.js.map +1 -0
  11. package/dist/adapters/node/host/node-command-port.d.ts +16 -0
  12. package/dist/adapters/node/host/node-command-port.d.ts.map +1 -1
  13. package/dist/adapters/node/host/node-command-port.js +74 -3
  14. package/dist/adapters/node/host/node-command-port.js.map +1 -1
  15. package/dist/adapters/node/host/node-persistent-shell.d.ts +35 -0
  16. package/dist/adapters/node/host/node-persistent-shell.d.ts.map +1 -0
  17. package/dist/adapters/node/host/node-persistent-shell.js +182 -0
  18. package/dist/adapters/node/host/node-persistent-shell.js.map +1 -0
  19. package/dist/adapters/node/tools/tool-executor.d.ts.map +1 -1
  20. package/dist/adapters/node/tools/tool-executor.js +10 -1
  21. package/dist/adapters/node/tools/tool-executor.js.map +1 -1
  22. package/dist/adapters/node/tools/workspace-review.d.ts +11 -0
  23. package/dist/adapters/node/tools/workspace-review.d.ts.map +1 -1
  24. package/dist/adapters/node/tools/workspace-review.js +61 -12
  25. package/dist/adapters/node/tools/workspace-review.js.map +1 -1
  26. package/dist/runtime/run-controller.d.ts.map +1 -1
  27. package/dist/runtime/run-controller.js +6 -1
  28. package/dist/runtime/run-controller.js.map +1 -1
  29. package/dist/tools/tool-registry.js +2 -2
  30. package/dist/tools/tool-registry.js.map +1 -1
  31. package/dist/tools/tool-timeouts.d.ts +21 -0
  32. package/dist/tools/tool-timeouts.d.ts.map +1 -0
  33. package/dist/tools/tool-timeouts.js +26 -0
  34. package/dist/tools/tool-timeouts.js.map +1 -0
  35. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ # 2026-09-30 — Baseline review chỉ tính file nguồn, không chết vì install/build chạy nền
2
+
3
+ Vùng: host (workspace review)
4
+
5
+ ## Trước
6
+ - `NodeWorkspaceReviewExecutor.create` so sánh `stateVersion` của hai lần capture, mà
7
+ `stateVersion` gộp **cả entry `derived`** (`node_modules`, `dist`, cache). Chỉ cần
8
+ `bun install` hoặc build đang chạy nền ghi vào các đường dẫn đó là baseline bị coi là
9
+ "đã đổi" ⇒ factory ném `Error` **ngoài mọi tool call** ⇒ cả run chết với
10
+ `Workspace changed while capturing review baseline. Retry when files are stable.`
11
+ Đo được: một run E2E 25 bước dừng ở bước 4 vì đúng lý do này.
12
+
13
+ ## Sau
14
+ - Baseline dùng `durableBaselineFingerprint`: chỉ băm các entry `evidenceClass === "durable"`
15
+ (đúng tập mà review được phép báo cáo), nên thay đổi ở đường dẫn generated **không** còn
16
+ làm hỏng baseline.
17
+ - Việc lấy baseline được **thử lại 3 lần** (chờ 250ms × lần) trước khi bỏ cuộc, và thông báo
18
+ lỗi nói rõ nguyên nhân (tiến trình nền còn ghi file nguồn) kèm fingerprint rút gọn để đối chiếu.
19
+
20
+ ## Test hồi quy
21
+ - `test/workspace-generated-state.test.ts`: "a background write to a generated path does not
22
+ invalidate the review baseline" — một writer ghi liên tục vào `node_modules/pkg/index.js`
23
+ trong lúc `create` chạy.
@@ -0,0 +1,15 @@
1
+ # 2026-09-30 — Tài liệu credential dùng chung cho mọi Galaxy host
2
+
3
+ Vùng: adapters/node/config
4
+
5
+ ## Thêm
6
+ - `galaxy-credentials`: một tài liệu YAML chỉ chủ sở hữu đọc được
7
+ (`~/.galaxy/credentials.yaml`, shape `version: 1`, `refs: <ref> -> <secret>`) để desktop app,
8
+ CLI, extension VS Code và web GUI cùng đọc/ghi một nguồn secret. Module sửa theo dòng thay vì
9
+ parse/ghi lại YAML để không phá comment/định dạng của writer khác.
10
+ - Test `test/galaxy-credentials.test.ts`.
11
+
12
+ ## Vì sao cần phát hành
13
+ - CLI 2.0.3 (đã lên npm) import `@galaxy-stack/ai-coder-core/adapters/node/config/galaxy-credentials`;
14
+ core 0.3.2 chưa có module này nên bản CLI đó crash lúc khởi động
15
+ (`ERR_MODULE_NOT_FOUND`). 0.3.3 đưa module vào bản phát hành, và CLI 2.0.4 pin core 0.3.3.
@@ -0,0 +1,25 @@
1
+ # 2026-09-30 — Shell persistent cho run_command, giống DeepSeek Harness
2
+
3
+ Vùng: host (command port), runtime
4
+
5
+ ## Trước
6
+ - Mỗi lệnh `run_command` spawn một tiến trình mới: `cd`/biến môi trường không giữ, tiến trình nền
7
+ (dev server, watcher) chết cùng lệnh, và không có gì dọn chúng khi bước kết thúc — đo được cảnh
8
+ `vite`/`bun run dev` còn sống sót qua nhiều run và tranh chấp với run sau.
9
+ - Interface `CommandSessionPort` (start/read/write/interrupt/kill/list) đã có trong core nhưng
10
+ **chưa có implementation** nào ở host; tool `manage_session` chỉ chạy được với contract double.
11
+
12
+ ## Sau
13
+ - Thêm `PersistentShell`: **một shell cho mỗi run**, đúng mô hình `@deepseek-ai/dsh-tool-bash-persistent`:
14
+ - lệnh chia sẻ `cwd`, biến đã `export`, và tiến trình nền; các lệnh **tuần tự hoá**;
15
+ - `timeoutMs` vẫn là **maximum**: xong sớm trả sớm; chạm trần thì **kill shell + trả partial**
16
+ và lần chạy sau bắt đầu shell mới (reset), đúng như harness;
17
+ - chỉ `cd` khi caller truyền `cwd` — không có `cwd` thì lệnh chạy ở nơi lệnh trước để lại;
18
+ - stdout/stderr **gộp một luồng** như tool bash của harness.
19
+ - `NodeCommandPort` dùng shell này khi command containment không bắt buộc (macOS/best-effort),
20
+ và có `dispose()`; CLI gọi `dispose()` khi run kết thúc nên **mọi thứ bước đó khởi động đều bị dừng**.
21
+ - Profile containment `required` giữ nguyên đường spawn một lần (không thể host shell dùng chung).
22
+
23
+ ## Test hồi quy
24
+ - `test/persistent-shell.test.ts`: cwd/biến/tiến trình nền giữ qua các lệnh; lệnh chạm trần bị kill
25
+ và trả partial rồi shell được reset.
@@ -0,0 +1,23 @@
1
+ # 2026-09-30 — Agent tự quyết timeout của lệnh, host chỉ giữ backstop suy ra từ lựa chọn đó
2
+
3
+ Vùng: tools (command.run, project.validate), runtime
4
+
5
+ ## Trước
6
+ - Schema của `run_command`/`project.validate` khoá `timeoutMs` ở `maximum: 600000`, và runtime
7
+ bọc mỗi lần dispatch bằng một hạn mức **cứng** đúng 600000ms. Model xin 600000ms cho một lệnh
8
+ scaffolder chạy lâu hơn ⇒ hai hạn mức bằng nhau ⇒ host luôn thắng ⇒ bước chết với
9
+ `Tool run_command did not return within 600000ms` thay vì nhận kết quả timeout để xử lý tiếp.
10
+ - Đo được: run E2E chết ở bước 1 vì đúng lý do này (`bunx orbit-cli new`).
11
+
12
+ ## Sau
13
+ - **Agent tự quyết**: schema không còn `maximum`, mô tả nói rõ phải chọn theo lệnh (scaffolder,
14
+ typecheck + verify, test dài cần vài phút), mặc định 120000ms; executor truyền thẳng giá trị
15
+ model chọn, không kẹp.
16
+ - **Host chỉ giữ backstop suy ra**: `dispatchGuardMs(requested) = requested + 60s` (mặc định
17
+ `120s + 60s`), nên timeout của chính tool luôn nổ trước và trả kết quả bình thường; hạn cứng
18
+ duy nhất còn lại là deadline của run (`--run-minutes`), và port lệnh vốn đã tự kẹp theo deadline.
19
+ - Thêm `description` (tuỳ chọn) vào schema `run_command` để lệnh có mô tả ngắn hiện trong transcript.
20
+
21
+ ## Test hồi quy
22
+ - `test/tool-timeouts.test.ts`: guard suy ra từ timeout agent chọn (600s → 660s, 900s → 960s), và
23
+ trường hợp không truyền timeout thì dùng mặc định + head-room.
@@ -0,0 +1,26 @@
1
+ # 2026-09-30 — Timeout của run_command theo đúng cơ chế DeepSeek Harness
2
+
3
+ Vùng: host (command port), tools
4
+
5
+ ## Bối cảnh
6
+ Đọc `@deepseek-ai/dsh-tool-bash-persistent` và `@deepseek-ai/dsh-tool-call-timeout-policy`:
7
+ - Tool `bash` có **một** tham số `command`; `timeoutMs` là **config của tool**, mặc định **300000ms (5 phút)**.
8
+ - Khi chạm trần: **kill và reset shell**, trả về partial output kèm câu
9
+ *"Your command timed out after N seconds or experienced an OOM error. Below is partial output:"*
10
+ và marker `[Command timed out or OOM]`.
11
+ - Policy của wrapper: *"A tool declares timeoutMs and promises to honor exec.signal; this wrapper
12
+ arms that deadline and maps its own expiry to TOOL_TIMEOUT **without racing or abandoning the tool
13
+ promise**"* — tức wrapper chỉ là backstop **cùng mốc**, không đua với tool.
14
+
15
+ ## Thay đổi
16
+ - `DEFAULT_TIMEOUT_MS` của command port: **120000 → 300000**, khớp DSH.
17
+ - Tóm tắt kết quả khi lệnh bị kill đổi thành đúng thông điệp DSH:
18
+ `Command timed out after <duration>s. Below is partial output:` (kèm stdout/stderr một phần,
19
+ `timedOut: true`), thay vì `Command timed_out with exit code -1.`.
20
+ - Guard của runtime giữ nguyên nguyên tắc DSH đã đối chiếu: **suy ra từ timeout của chính tool**
21
+ (requested + 60s) nên tool luôn trả kết quả trước; hạn cứng duy nhất là deadline của run.
22
+
23
+ ## Ghi chú
24
+ - Khác biệt còn lại so với DSH: DSH dùng **shell persistent** (một shell sống qua nhiều lệnh, có
25
+ session/queue theo agent); Galaxy hiện spawn tiến trình mới cho mỗi lệnh. Đây là hạng mục riêng,
26
+ cần thêm port + wiring + test nếu muốn parity đầy đủ.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  Times are recorded in Asia/Ho_Chi_Minh (+07:00). Per-fix fragments are filed in
8
8
  [CHANGELOG.d](CHANGELOG.d/README.md) and compiled here at release time.
9
9
 
10
+ ## [0.3.2] - 2026-09-30 14:50 +0700
11
+
12
+ ### Changed
13
+
14
+ - Baseline review chỉ tính file nguồn, không chết vì install/build chạy nền.
15
+ - Shell persistent cho run_command, giống DeepSeek Harness.
16
+ - Agent tự quyết timeout của lệnh, host chỉ giữ backstop suy ra từ lựa chọn đó.
17
+ - Timeout của run_command theo đúng cơ chế DeepSeek Harness.
18
+
19
+ ## [0.3.3] - 2026-09-30 18:07 +0700
20
+
21
+ ### Changed
22
+
23
+ - Baseline review chỉ tính file nguồn, không chết vì install/build chạy nền.
24
+ - Tài liệu credential dùng chung cho mọi Galaxy host.
25
+ - Shell persistent cho run_command, giống DeepSeek Harness.
26
+ - Agent tự quyết timeout của lệnh, host chỉ giữ backstop suy ra từ lựa chọn đó.
27
+ - Timeout của run_command theo đúng cơ chế DeepSeek Harness.
28
+
10
29
  ## [0.3.1] - 2026-09-29 05:25 +07:00
11
30
 
12
31
  ### Fixed
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Galaxy keeps every provider secret in one owner-only YAML document,
3
+ * `~/.galaxy/credentials.yaml`, written in the shape the DeepSeek Harness
4
+ * credential store reads (`version: 1`, `refs: <ref> -> <secret>`). Pointing DSH
5
+ * at this file is a one-row profile patch, so the desktop app, the CLI, the VS
6
+ * Code extension and the web GUI all read and write the same secrets.
7
+ *
8
+ * The editor works on lines instead of round-tripping YAML on purpose: the file
9
+ * is shared with another writer that preserves comments and formatting, and a
10
+ * full parse/render would silently reformat or drop entries this module does not
11
+ * own (including DSH's `records` block).
12
+ */
13
+ /** File name inside the Galaxy home. */
14
+ export declare const GALAXY_CREDENTIALS_FILENAME = "credentials.yaml";
15
+ /** Document version this module writes; DSH refuses an unversioned document. */
16
+ export declare const GALAXY_CREDENTIALS_VERSION = 1;
17
+ /** Absent or unreadable documents read as empty rather than throwing. */
18
+ export interface GalaxyCredentialsDocument {
19
+ readonly refs: Readonly<Record<string, string>>;
20
+ readonly version: number | undefined;
21
+ }
22
+ /** Absolute path of the shared credential document. */
23
+ export declare function galaxyCredentialsPath(home?: string): string;
24
+ /**
25
+ * Reference name for one provider id: `galaxy` → `GBH_GALAXY_API_KEY`,
26
+ * `my-gateway` → `GBH_MY_GATEWAY_API_KEY`. DSH resolves a route's `apiKeyEnv`
27
+ * against these names, so the mapping has to be derivable, never hand-kept.
28
+ */
29
+ export declare function galaxyRefName(providerId: string): string;
30
+ /** Quote a scalar the way the shared document expects, only when it must. */
31
+ export declare function renderRefValue(value: string): string;
32
+ /** Parse the document's refs and version. A flat pre-version file still reads. */
33
+ export declare function parseGalaxyCredentials(text: string): GalaxyCredentialsDocument;
34
+ /** Read the document; a missing file is an empty document, never an error. */
35
+ export declare function readGalaxyCredentials(path?: string): GalaxyCredentialsDocument;
36
+ /** One reference value, or undefined when it is absent or empty. */
37
+ export declare function readGalaxyCredential(ref: string, path?: string): string | undefined;
38
+ /** Owner-only mode and the refs present, for `config doctor`. */
39
+ export interface GalaxyCredentialsHealth {
40
+ readonly exists: boolean;
41
+ readonly mode: string | undefined;
42
+ readonly ownerOnly: boolean;
43
+ readonly refs: readonly string[];
44
+ readonly version: number | undefined;
45
+ }
46
+ export declare function galaxyCredentialsHealth(path?: string): GalaxyCredentialsHealth;
47
+ /**
48
+ * Insert, replace, or remove one reference while leaving every other byte of the
49
+ * document — comments, formatting, and DSH's own `records` block — untouched.
50
+ *
51
+ * @param ref - reference name to write.
52
+ * @param value - secret value; an empty string removes the reference.
53
+ * @param path - document path, defaulting to the shared Galaxy home.
54
+ */
55
+ export declare function upsertGalaxyCredential(ref: string, value: string, path?: string): void;
56
+ /** Write several references in one locked pass. */
57
+ export declare function upsertGalaxyCredentials(entries: Readonly<Record<string, string>>, path?: string): void;
58
+ //# sourceMappingURL=galaxy-credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"galaxy-credentials.d.ts","sourceRoot":"","sources":["../../../../src/adapters/node/config/galaxy-credentials.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;GAWG;AAEH,wCAAwC;AACxC,eAAO,MAAM,2BAA2B,qBAAqB,CAAC;AAC9D,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,yEAAyE;AACzE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,uDAAuD;AACvD,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGxD;AAaD,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAyBD,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,yBAAyB,CAoB9E;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,MAAgC,GAAG,yBAAyB,CAMvG;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,MAAgC,GAAG,MAAM,GAAG,SAAS,CAG5G;AAED,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,MAAgC,GAAG,uBAAuB,CAcvG;AAsCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,MAAgC,GAAG,IAAI,CAgC/G;AAED,mDAAmD;AACnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,GAAE,MAAgC,GAAG,IAAI,CAE/H"}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @author Bùi Trọng Hiếu
3
+ * @email kevinbui210191@gmail.com
4
+ * @create date 2026-09-29
5
+ * @desc The Galaxy credential document — one secrets file for every Galaxy host.
6
+ */
7
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ /**
11
+ * Galaxy keeps every provider secret in one owner-only YAML document,
12
+ * `~/.galaxy/credentials.yaml`, written in the shape the DeepSeek Harness
13
+ * credential store reads (`version: 1`, `refs: <ref> -> <secret>`). Pointing DSH
14
+ * at this file is a one-row profile patch, so the desktop app, the CLI, the VS
15
+ * Code extension and the web GUI all read and write the same secrets.
16
+ *
17
+ * The editor works on lines instead of round-tripping YAML on purpose: the file
18
+ * is shared with another writer that preserves comments and formatting, and a
19
+ * full parse/render would silently reformat or drop entries this module does not
20
+ * own (including DSH's `records` block).
21
+ */
22
+ /** File name inside the Galaxy home. */
23
+ export const GALAXY_CREDENTIALS_FILENAME = "credentials.yaml";
24
+ /** Document version this module writes; DSH refuses an unversioned document. */
25
+ export const GALAXY_CREDENTIALS_VERSION = 1;
26
+ /** Absolute path of the shared credential document. */
27
+ export function galaxyCredentialsPath(home = homedir()) {
28
+ return join(home, ".galaxy", GALAXY_CREDENTIALS_FILENAME);
29
+ }
30
+ /**
31
+ * Reference name for one provider id: `galaxy` → `GBH_GALAXY_API_KEY`,
32
+ * `my-gateway` → `GBH_MY_GATEWAY_API_KEY`. DSH resolves a route's `apiKeyEnv`
33
+ * against these names, so the mapping has to be derivable, never hand-kept.
34
+ */
35
+ export function galaxyRefName(providerId) {
36
+ const normalized = providerId.trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
37
+ return `GBH_${normalized.length === 0 ? "PROVIDER" : normalized}_API_KEY`;
38
+ }
39
+ function unquote(raw) {
40
+ const value = raw.trim();
41
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
42
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
43
+ }
44
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'"))
45
+ return value.slice(1, -1).replace(/''/g, "'");
46
+ /* A plain scalar ends at an inline comment. */
47
+ const comment = value.search(/\s#/);
48
+ return (comment === -1 ? value : value.slice(0, comment)).trim();
49
+ }
50
+ /** Quote a scalar the way the shared document expects, only when it must. */
51
+ export function renderRefValue(value) {
52
+ return /^[A-Za-z0-9._@/+:-]+$/.test(value) ? value : `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
53
+ }
54
+ /** Locate the top-level `refs:` block and the entries inside it. */
55
+ function findRefsBlock(lines) {
56
+ for (let index = 0; index < lines.length; index += 1) {
57
+ if (!/^refs:\s*(#.*)?$/.test(lines[index] ?? ""))
58
+ continue;
59
+ const entries = [];
60
+ for (let scan = index + 1; scan < lines.length; scan += 1) {
61
+ const line = lines[scan] ?? "";
62
+ if (line.trim().length === 0 || line.trimStart().startsWith("#"))
63
+ continue;
64
+ const match = /^(\s+)([^\s#][^:]*):\s*(.*)$/.exec(line);
65
+ if (match === null)
66
+ break;
67
+ entries.push({ indent: match[1] ?? " ", key: unquote(match[2] ?? ""), line: scan });
68
+ }
69
+ return { header: index, entries };
70
+ }
71
+ return undefined;
72
+ }
73
+ /** Parse the document's refs and version. A flat pre-version file still reads. */
74
+ export function parseGalaxyCredentials(text) {
75
+ const lines = text.split(/\r?\n/);
76
+ const versionLine = lines.find(line => /^version:\s*\d+\s*$/.test(line));
77
+ const version = versionLine === undefined ? undefined : Number(versionLine.split(":")[1]?.trim());
78
+ const refs = {};
79
+ const block = findRefsBlock(lines);
80
+ if (block !== undefined) {
81
+ for (const entry of block.entries) {
82
+ const raw = lines[entry.line] ?? "";
83
+ refs[entry.key] = unquote(raw.slice(raw.indexOf(":") + 1));
84
+ }
85
+ return Object.freeze({ refs: Object.freeze(refs), version });
86
+ }
87
+ /* Pre-release flat layout: every top-level scalar is a reference. */
88
+ for (const line of lines) {
89
+ const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(\S.*)$/.exec(line);
90
+ if (match === null || match[1] === "version")
91
+ continue;
92
+ refs[match[1]] = unquote(match[2] ?? "");
93
+ }
94
+ return Object.freeze({ refs: Object.freeze(refs), version });
95
+ }
96
+ /** Read the document; a missing file is an empty document, never an error. */
97
+ export function readGalaxyCredentials(path = galaxyCredentialsPath()) {
98
+ try {
99
+ return parseGalaxyCredentials(readFileSync(path, "utf8"));
100
+ }
101
+ catch {
102
+ return Object.freeze({ refs: Object.freeze({}), version: undefined });
103
+ }
104
+ }
105
+ /** One reference value, or undefined when it is absent or empty. */
106
+ export function readGalaxyCredential(ref, path = galaxyCredentialsPath()) {
107
+ const value = readGalaxyCredentials(path).refs[ref];
108
+ return value !== undefined && value.length > 0 ? value : undefined;
109
+ }
110
+ export function galaxyCredentialsHealth(path = galaxyCredentialsPath()) {
111
+ try {
112
+ const info = statSync(path);
113
+ const document = readGalaxyCredentials(path);
114
+ return Object.freeze({
115
+ exists: true,
116
+ mode: (info.mode & 0o777).toString(8),
117
+ ownerOnly: (info.mode & 0o077) === 0,
118
+ refs: Object.freeze(Object.keys(document.refs).sort()),
119
+ version: document.version,
120
+ });
121
+ }
122
+ catch {
123
+ return Object.freeze({ exists: false, mode: undefined, ownerOnly: true, refs: Object.freeze([]), version: undefined });
124
+ }
125
+ }
126
+ function sleepSync(ms) {
127
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
128
+ }
129
+ /** Cross-process writer lock beside the document, so two hosts never interleave. */
130
+ function withLock(path, run) {
131
+ const lock = path + ".lock";
132
+ mkdirSync(dirname(path), { recursive: true });
133
+ for (let attempt = 0; attempt < 40; attempt += 1) {
134
+ try {
135
+ const handle = openSync(lock, "wx", 0o600);
136
+ closeSync(handle);
137
+ try {
138
+ return run();
139
+ }
140
+ finally {
141
+ try {
142
+ unlinkSync(lock);
143
+ }
144
+ catch {
145
+ /* a peer already released it */
146
+ }
147
+ }
148
+ }
149
+ catch (error) {
150
+ const code = error.code;
151
+ if (code !== "EEXIST")
152
+ throw error;
153
+ /* Break a stale lock left by a crashed writer. */
154
+ try {
155
+ if (Date.now() - statSync(lock).mtimeMs > 10_000)
156
+ unlinkSync(lock);
157
+ }
158
+ catch {
159
+ /* the lock disappeared meanwhile */
160
+ }
161
+ sleepSync(50);
162
+ }
163
+ }
164
+ throw new Error(`credentials: could not acquire ${lock}`);
165
+ }
166
+ /**
167
+ * Insert, replace, or remove one reference while leaving every other byte of the
168
+ * document — comments, formatting, and DSH's own `records` block — untouched.
169
+ *
170
+ * @param ref - reference name to write.
171
+ * @param value - secret value; an empty string removes the reference.
172
+ * @param path - document path, defaulting to the shared Galaxy home.
173
+ */
174
+ export function upsertGalaxyCredential(ref, value, path = galaxyCredentialsPath()) {
175
+ withLock(path, () => {
176
+ const exists = existsSync(path);
177
+ const text = exists ? readFileSync(path, "utf8") : "";
178
+ const trailingNewline = text.length === 0 || text.endsWith("\n");
179
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
180
+ if (lines.length > 0 && lines[lines.length - 1] === "")
181
+ lines.pop();
182
+ const block = findRefsBlock(lines);
183
+ const encoded = value.length === 0 ? undefined : renderRefValue(value);
184
+ if (block === undefined) {
185
+ if (!lines.some(line => /^version:\s*\d+\s*$/.test(line)))
186
+ lines.unshift(`version: ${GALAXY_CREDENTIALS_VERSION}`);
187
+ lines.push("refs:");
188
+ if (encoded !== undefined)
189
+ lines.push(` ${ref}: ${encoded}`);
190
+ }
191
+ else {
192
+ const existing = block.entries.find(entry => entry.key === ref);
193
+ if (existing !== undefined) {
194
+ if (encoded === undefined)
195
+ lines.splice(existing.line, 1);
196
+ else
197
+ lines[existing.line] = `${existing.indent}${ref}: ${encoded}`;
198
+ }
199
+ else if (encoded !== undefined) {
200
+ const indent = block.entries[0]?.indent ?? " ";
201
+ const lastLine = block.entries.length === 0 ? block.header : block.entries[block.entries.length - 1].line;
202
+ lines.splice(lastLine + 1, 0, `${indent}${ref}: ${encoded}`);
203
+ }
204
+ }
205
+ const rendered = lines.join("\n") + (trailingNewline ? "\n" : "");
206
+ const temporary = `${path}.${process.pid.toString(36)}.tmp`;
207
+ writeFileSync(temporary, rendered, { encoding: "utf8", mode: 0o600 });
208
+ renameSync(temporary, path);
209
+ });
210
+ }
211
+ /** Write several references in one locked pass. */
212
+ export function upsertGalaxyCredentials(entries, path = galaxyCredentialsPath()) {
213
+ for (const [ref, value] of Object.entries(entries))
214
+ upsertGalaxyCredential(ref, value, path);
215
+ }
216
+ //# sourceMappingURL=galaxy-credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"galaxy-credentials.js","sourceRoot":"","sources":["../../../../src/adapters/node/config/galaxy-credentials.ts"],"names":[],"mappings":"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"}
@@ -5,11 +5,27 @@ export declare class NodeCommandPort implements CommandRunnerPort {
5
5
  private readonly scope;
6
6
  readonly containmentStatus: CommandContainmentStatus;
7
7
  readonly hostEnvironment: ReturnType<typeof nodeCommandHostEnvironment>;
8
+ /**
9
+ * One shell per run, like @deepseek-ai/dsh-tool-bash-persistent: commands share cwd,
10
+ * exports and background jobs, calls are serialized, a command that hits its maximum is
11
+ * killed with its partial output returned, and disposing the port at the end of a run
12
+ * stops whatever the step left running. Used whenever command containment is not
13
+ * mandatory, because a contained one-shot spawn cannot host a shared shell.
14
+ */
15
+ private readonly shell;
8
16
  private constructor();
9
17
  static create(workspaceRoot: string, options?: Readonly<{
10
18
  containment?: CommandContainmentMode;
11
19
  }>): Promise<NodeCommandPort>;
12
20
  static probeContainment(): Promise<CommandContainmentProbeResult>;
21
+ /**
22
+ * Run through the shared shell: cwd, exports and background jobs persist between calls,
23
+ * calls are serialized, and the timeout is a maximum whose expiry kills the shell and
24
+ * returns the partial output (the harness behaves the same way).
25
+ */
26
+ private runInPersistentShell;
27
+ /** Stop the shared shell and anything it started; call this when a run ends. */
28
+ dispose(): Promise<void>;
13
29
  run(input: Readonly<{
14
30
  command: string;
15
31
  cwd?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"node-command-port.d.ts","sourceRoot":"","sources":["../../../../src/adapters/node/host/node-command-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGhB,UAAU,EACV,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAgHnE,qBAAa,eAAgB,YAAW,iBAAiB;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,QAAQ,CAAC,iBAAiB,EAAE,wBAAwB;IACpD,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC;IAHzE,OAAO;WAMM,MAAM,CACjB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,QAAQ,CAAC;QAAE,WAAW,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAM,GAC/D,OAAO,CAAC,eAAe,CAAC;WAYd,gBAAgB,IAAI,OAAO,CAAC,6BAA6B,CAAC;IAIjE,GAAG,CACP,KAAK,EAAE,QAAQ,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACvC,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,EACF,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;CA2IzC"}
1
+ {"version":3,"file":"node-command-port.d.ts","sourceRoot":"","sources":["../../../../src/adapters/node/host/node-command-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGhB,UAAU,EACV,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAoHnE,qBAAa,eAAgB,YAAW,iBAAiB;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,QAAQ,CAAC,iBAAiB,EAAE,wBAAwB;IACpD,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC;IACvE;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK;IAXxB,OAAO;WAcM,MAAM,CACjB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,QAAQ,CAAC;QAAE,WAAW,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAM,GAC/D,OAAO,CAAC,eAAe,CAAC;WAiBd,gBAAgB,IAAI,OAAO,CAAC,6BAA6B,CAAC;IAIvE;;;;OAIG;YACW,oBAAoB;IAiDlC,gFAAgF;IAC1E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,GAAG,CACP,KAAK,EAAE,QAAQ,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACvC,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,EACF,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;CAgJzC"}
@@ -2,8 +2,12 @@ import { spawn } from "node:child_process";
2
2
  import { lstat } from "node:fs/promises";
3
3
  import { assertWorkspaceHasNoHardLinks, buildBubblewrapArguments, containmentStatus, probeCommandContainment, } from "./command-containment.js";
4
4
  import { nodeCommandHostEnvironment } from "./host-environment.js";
5
+ import { PersistentShell } from "./node-persistent-shell.js";
5
6
  import { WorkspaceScope } from "./path-scope.js";
6
- const DEFAULT_TIMEOUT_MS = 120_000;
7
+ // Matches the harness default this project mirrors (@deepseek-ai/dsh-tool-bash-persistent
8
+ // declares timeoutMs 300000): a command may run for five minutes unless the caller asks for
9
+ // more or less, and the timeout is a maximum, not a wait.
10
+ const DEFAULT_TIMEOUT_MS = 300_000;
7
11
  const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
8
12
  const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
9
13
  function failure(code, message, retryable = false) {
@@ -115,10 +119,20 @@ export class NodeCommandPort {
115
119
  scope;
116
120
  containmentStatus;
117
121
  hostEnvironment;
118
- constructor(scope, containmentStatus, hostEnvironment) {
122
+ shell;
123
+ constructor(scope, containmentStatus, hostEnvironment,
124
+ /**
125
+ * One shell per run, like @deepseek-ai/dsh-tool-bash-persistent: commands share cwd,
126
+ * exports and background jobs, calls are serialized, a command that hits its maximum is
127
+ * killed with its partial output returned, and disposing the port at the end of a run
128
+ * stops whatever the step left running. Used whenever command containment is not
129
+ * mandatory, because a contained one-shot spawn cannot host a shared shell.
130
+ */
131
+ shell) {
119
132
  this.scope = scope;
120
133
  this.containmentStatus = containmentStatus;
121
134
  this.hostEnvironment = hostEnvironment;
135
+ this.shell = shell;
122
136
  }
123
137
  static async create(workspaceRoot, options = {}) {
124
138
  const mode = options.containment ?? "best_effort";
@@ -129,11 +143,63 @@ export class NodeCommandPort {
129
143
  WorkspaceScope.create(workspaceRoot),
130
144
  probeCommandContainment(),
131
145
  ]);
132
- return new NodeCommandPort(scope, containmentStatus(mode, probe), nodeCommandHostEnvironment());
146
+ return new NodeCommandPort(scope, containmentStatus(mode, probe), nodeCommandHostEnvironment(), new PersistentShell({ root: workspaceRoot }));
133
147
  }
134
148
  static async probeContainment() {
135
149
  return await probeCommandContainment();
136
150
  }
151
+ /**
152
+ * Run through the shared shell: cwd, exports and background jobs persist between calls,
153
+ * calls are serialized, and the timeout is a maximum whose expiry kills the shell and
154
+ * returns the partial output (the harness behaves the same way).
155
+ */
156
+ async runInPersistentShell(input, context) {
157
+ try {
158
+ const cwd = await this.scope.resolveExisting(input.cwd ?? ".");
159
+ if (!(await lstat(cwd)).isDirectory()) {
160
+ return failure("INVALID_INPUT", "Command cwd must be a workspace directory.");
161
+ }
162
+ const requestedTimeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
163
+ if (!Number.isSafeInteger(requestedTimeout) || requestedTimeout < 1) {
164
+ return failure("INVALID_INPUT", "timeoutMs must be a positive integer.");
165
+ }
166
+ const remaining = context.deadline - Date.now();
167
+ if (remaining <= 0)
168
+ return failure("DEADLINE_EXCEEDED", "The run deadline elapsed.");
169
+ const result = await this.shell.run(input.command, {
170
+ // Only an explicit cwd moves the shell; without one the command runs where the
171
+ // previous command left the shell, which is what makes the session persistent.
172
+ ...(input.cwd === undefined ? {} : { cwd }),
173
+ ...(input.env === undefined ? {} : { env: input.env }),
174
+ ...(input.maxOutputBytes === undefined ? {} : { maxOutputBytes: input.maxOutputBytes }),
175
+ timeoutMs: Math.min(requestedTimeout, remaining),
176
+ }, context.signal);
177
+ if (result.status === "unavailable") {
178
+ return failure("UNAVAILABLE", "The persistent shell could not be started.");
179
+ }
180
+ return Object.freeze({
181
+ ok: true,
182
+ data: Object.freeze({
183
+ command: input.command,
184
+ durationMs: result.durationMs,
185
+ exitCode: result.exitCode,
186
+ // The shared shell merges both streams, like the harness tool does.
187
+ stderr: "",
188
+ stderrTruncated: false,
189
+ stdout: result.output,
190
+ stdoutTruncated: result.truncated,
191
+ status: result.status,
192
+ }),
193
+ });
194
+ }
195
+ catch (error) {
196
+ return exceptionFailure(error);
197
+ }
198
+ }
199
+ /** Stop the shared shell and anything it started; call this when a run ends. */
200
+ async dispose() {
201
+ await this.shell.dispose();
202
+ }
137
203
  async run(input, context) {
138
204
  if (context.signal.aborted)
139
205
  return failure("CANCELED", "The run was canceled.");
@@ -142,6 +208,11 @@ export class NodeCommandPort {
142
208
  if (typeof input.command !== "string" || input.command.trim().length === 0 || input.command.length > 32_768) {
143
209
  return failure("INVALID_INPUT", "Command must contain between 1 and 32768 characters.");
144
210
  }
211
+ // A mandatory confinement wraps each spawn, so only the best-effort profile can host the
212
+ // shared shell the harness uses; everything else keeps the one-shot path below.
213
+ if (this.containmentStatus.mode !== "required") {
214
+ return await this.runInPersistentShell(input, context);
215
+ }
145
216
  try {
146
217
  const cwd = await this.scope.resolveExisting(input.cwd ?? ".");
147
218
  if (!(await lstat(cwd)).isDirectory()) {