From 575db56619ba273483209f47b4ffdc30b5ef5b95 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 14 Jul 2026 09:13:03 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/xlabtg/teleton-agent/issues/712 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..54ab35df --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-07-14T09:13:03.183Z for PR creation at branch issue-712-916121224ac9 for issue https://github.com/xlabtg/teleton-agent/issues/712 \ No newline at end of file From 4a6a171eaf57d5f281a18d0f634c4f7660a948ad Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 14 Jul 2026 09:30:42 +0000 Subject: [PATCH 2/3] fix(gocoon): restrict wallet data permissions --- src/gocoon/__tests__/cli-permissions.test.ts | 100 +++++++++++++++++++ src/gocoon/cli.ts | 13 ++- 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/gocoon/__tests__/cli-permissions.test.ts diff --git a/src/gocoon/__tests__/cli-permissions.test.ts b/src/gocoon/__tests__/cli-permissions.test.ts new file mode 100644 index 00000000..8a35b6f0 --- /dev/null +++ b/src/gocoon/__tests__/cli-permissions.test.ts @@ -0,0 +1,100 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; + +const { tempRoot, runGocoonMock } = vi.hoisted(() => ({ + tempRoot: require("fs").mkdtempSync( + require("path").join(require("os").tmpdir(), "teleton-gocoon-permissions-") + ) as string, + runGocoonMock: vi.fn(), +})); + +vi.mock("../../workspace/paths.js", () => ({ TELETON_ROOT: tempRoot })); + +vi.mock("../installer.js", () => ({ + ensureGocoonBinaries: vi.fn(async () => ({ gocoon: "/tmp/gocoon" })), +})); + +vi.mock("child_process", () => ({ + execFile: (...args: unknown[]) => runGocoonMock(...args), + spawn: vi.fn(), +})); + +const { gocoonInit } = await import("../cli.js"); +const { clientConfigPath, gocoonDataDir, walletPath } = await import("../paths.js"); + +const mode = (path: string): number => statSync(path).mode & 0o777; +type ExecCallback = (error: Error | null, result: { stdout: string; stderr: string }) => void; + +describe("gocoon data permissions", () => { + beforeEach(() => { + rmSync(gocoonDataDir(), { recursive: true, force: true }); + runGocoonMock.mockReset(); + }); + + afterAll(() => { + rmSync(tempRoot, { recursive: true, force: true }); + }); + + it("creates the gocoon data directory with mode 0o700", async () => { + const previousUmask = process.umask(0o022); + runGocoonMock.mockImplementation( + (_file: string, _args: string[], _options: unknown, callback: ExecCallback) => { + mkdirSync(gocoonDataDir(), { recursive: true }); + writeFileSync(walletPath(), "{}", { mode: 0o600 }); + callback(null, { + stdout: JSON.stringify({ fund_address: "fund", owner_address: "owner" }), + stderr: "", + }); + } + ); + + try { + await gocoonInit(); + expect(mode(gocoonDataDir())).toBe(0o700); + expect(mode(walletPath())).toBe(0o600); + } finally { + process.umask(previousUmask); + } + }); + + it("tightens an existing gocoon data directory to mode 0o700", async () => { + mkdirSync(gocoonDataDir(), { recursive: true }); + chmodSync(gocoonDataDir(), 0o755); + runGocoonMock.mockImplementation( + (_file: string, _args: string[], _options: unknown, callback: ExecCallback) => + callback(null, { + stdout: JSON.stringify({ fund_address: "fund", owner_address: "owner" }), + stderr: "", + }) + ); + + await gocoonInit(); + + expect(mode(gocoonDataDir())).toBe(0o700); + }); + + it("tightens an existing wallet to mode 0o600 when it is reused", async () => { + mkdirSync(gocoonDataDir(), { recursive: true }); + writeFileSync(walletPath(), "{}", { mode: 0o644 }); + writeFileSync(clientConfigPath(), "{}", { mode: 0o600 }); + chmodSync(walletPath(), 0o644); + runGocoonMock.mockImplementation( + (_file: string, _args: string[], _options: unknown, callback: ExecCallback) => + callback(null, { + stdout: JSON.stringify({ + fund_address: "fund", + owner_address: "owner", + balance_nano: "0", + }), + stderr: "", + }) + ); + + expect(existsSync(walletPath())).toBe(true); + await gocoonInit(); + + expect(mode(walletPath())).toBe(0o600); + }); +}); diff --git a/src/gocoon/cli.ts b/src/gocoon/cli.ts index 54792039..7f40d176 100644 --- a/src/gocoon/cli.ts +++ b/src/gocoon/cli.ts @@ -1,6 +1,6 @@ import { execFile, spawn } from "child_process"; import { promisify } from "util"; -import { existsSync, mkdirSync } from "fs"; +import { chmodSync, existsSync, mkdirSync } from "fs"; import { createLogger } from "../utils/logger.js"; import { fetchWithTimeout } from "../utils/fetch.js"; import { ensureGocoonBinaries } from "./installer.js"; @@ -15,6 +15,8 @@ import { const log = createLogger("gocoon"); const execFileAsync = promisify(execFile); const MAX_BUFFER = 8 * 1024 * 1024; +const PRIVATE_DIR_MODE = 0o700; +const PRIVATE_FILE_MODE = 0o600; // Run a gocoon subcommand and capture stdout (for --json / short commands). export async function runGocoon(args: string[], timeoutMs = 60_000): Promise { @@ -68,8 +70,15 @@ export function walletExists(): boolean { return existsSync(walletPath()) && existsSync(clientConfigPath()); } +function hardenGocoonDataPermissions(): void { + mkdirSync(gocoonDataDir(), { recursive: true, mode: PRIVATE_DIR_MODE }); + chmodSync(gocoonDataDir(), PRIVATE_DIR_MODE); + if (existsSync(walletPath())) chmodSync(walletPath(), PRIVATE_FILE_MODE); +} + // Create the COCOON wallet, or reuse the existing one (stable funding address). export async function gocoonInit(): Promise { + hardenGocoonDataPermissions(); if (walletExists()) { const info = await walletInfo(); return { @@ -81,10 +90,10 @@ export async function gocoonInit(): Promise { configPath: clientConfigPath(), }; } - mkdirSync(gocoonDataDir(), { recursive: true }); const j = parseJson( await runGocoon(["init", "--dir", gocoonDataDir(), "--json", "--force"], 120_000) ); + hardenGocoonDataPermissions(); return { fundAddress: String(j.fund_address ?? ""), ownerAddress: String(j.owner_address ?? ""), From 18e29bfdcf18a94eab043aa44b87d296a905be4f Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 14 Jul 2026 09:35:50 +0000 Subject: [PATCH 3/3] chore: remove pull request placeholder --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 54ab35df..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-07-14T09:13:03.183Z for PR creation at branch issue-712-916121224ac9 for issue https://github.com/xlabtg/teleton-agent/issues/712 \ No newline at end of file