mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
fix: refuse to git fetch while the Docker case list cannot be read (release review)
readDockerCases() reads an unreadable or half-written docker-cases.json as [], so a fetch (which runs repository hooks, and runs unattended on the interval) could run inside a Docker workspace. The fetch paths now read it through a strict reader: the Fetch button answers a failed run (state 'error') and the scheduler skips the tick, undoes its picks and logs once. The status and diff routes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b89f15c12f
commit
1752d7ee2c
8 files changed
+221
-18
No files matched your search
@@ -17,7 +17,7 @@ import type { FastifyInstance } from 'fastify';
|
||||
import { ApiErrorCode, createErrorResponse, getErrorMessage, type ApiResponse } from '../../types.js';
|
||||
import { redactGitCredentials } from '../../git-clone.js';
|
||||
import { emptyReport, fetchWorkspaceOnce, gitFetchStatusFrom, type GitFetchReport } from '../../git-fetch.js';
|
||||
import { readDockerCases } from '../../docker-hosts.js';
|
||||
import { readDockerCasesOrThrow } from '../../docker-hosts.js';
|
||||
import { getDataDir } from '../../config/instance.js';
|
||||
import { findSessionOrFail, persistAndBroadcastSession } from '../route-helpers.js';
|
||||
import {
|
||||
@@ -38,9 +38,18 @@ import type { EventPort, SessionPort } from '../ports/index.js';
|
||||
* Host paths of every Docker case workspace: repositories at or inside these are never inspected.
|
||||
* Exported so the interval scheduler (src/git-fetch-scheduler.ts) uses the SAME list — the Docker rule has
|
||||
* to be one rule, checked by every path that could run git.
|
||||
*
|
||||
* REJECTS when `docker-cases.json` exists but cannot be read or parsed (its writer truncates and rewrites it
|
||||
* in place, so a read can land mid-write). The fetch paths fail closed on that: a fetch runs a repository's
|
||||
* own hooks, and without the list nothing tells which folders a container can write to. The read-only
|
||||
* status and diff routes read it as no Docker workspaces, as they always have.
|
||||
*/
|
||||
export const defaultDockerWorkspaces = async (): Promise<string[]> =>
|
||||
(await readDockerCases(getDataDir()).catch(() => [])).map((c) => c.hostWorkspacePath).filter(Boolean);
|
||||
(await readDockerCasesOrThrow(getDataDir())).map((c) => c.hostWorkspacePath).filter(Boolean);
|
||||
|
||||
/** The read-only routes' reading of the list: unchanged, an unreadable list is no Docker workspaces. */
|
||||
const dockerWorkspacesOrNone = (dockerWorkspaces: () => Promise<string[]>): Promise<string[]> =>
|
||||
dockerWorkspaces().catch(() => []);
|
||||
|
||||
/**
|
||||
* The repository limit and git timeout a request asks for (Settings → Bottom bar, per device):
|
||||
@@ -78,7 +87,7 @@ export function registerGitStatusRoutes(
|
||||
git,
|
||||
fresh: fresh === '1',
|
||||
...limits,
|
||||
dockerWorkspaces: await dockerWorkspaces(),
|
||||
dockerWorkspaces: await dockerWorkspacesOrNone(dockerWorkspaces),
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -102,7 +111,7 @@ export function registerGitStatusRoutes(
|
||||
? await findWorkspaceRepo(session.workingDir, repo, {
|
||||
git,
|
||||
...limits,
|
||||
dockerWorkspaces: await dockerWorkspaces(),
|
||||
dockerWorkspaces: await dockerWorkspacesOrNone(dockerWorkspaces),
|
||||
})
|
||||
: null;
|
||||
const status = repoRoot
|
||||
@@ -150,9 +159,24 @@ export function registerGitStatusRoutes(
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote) return { success: true, data: emptyReport('unsupported', { reason: 'remote' }) };
|
||||
if (session.docker) return { success: true, data: emptyReport('unsupported', { reason: 'docker' }) };
|
||||
// Fail CLOSED on a Docker case list that cannot be read: a fetch runs the repository's hooks, so it never
|
||||
// runs without knowing which folders a container can write to. Answered as a failed run (`state: 'error'`),
|
||||
// published like one, so the Session Options Git row shows it.
|
||||
let dockerRoots: string[];
|
||||
try {
|
||||
dockerRoots = await dockerWorkspaces();
|
||||
} catch (err) {
|
||||
console.warn(`[git fetch] Docker case list unreadable, nothing fetched: ${getErrorMessage(err)}`);
|
||||
const report = emptyReport('error', {
|
||||
error:
|
||||
'Nothing was fetched: Codeman could not read its Docker case list (docker-cases.json), so it cannot tell which folders a container can write to.',
|
||||
});
|
||||
if (session.setGitFetchStatus(gitFetchStatusFrom(report, 'click'))) persistAndBroadcastSession(ctx, session);
|
||||
return { success: true, data: report };
|
||||
}
|
||||
const run = await fetchWorkspaceOnce(
|
||||
session.workingDir,
|
||||
{ git, ...limits, dockerWorkspaces: await dockerWorkspaces() },
|
||||
{ git, ...limits, dockerWorkspaces: dockerRoots },
|
||||
'click'
|
||||
);
|
||||
// The response answers the click, and the run is also the session's current truth: what the panel's
|
||||
|
||||
+2
-1
@@ -406,7 +406,8 @@ export class WebServer extends EventEmitter {
|
||||
sessionIds: () => this.sessions.keys(),
|
||||
getSession: (id) => this.sessions.get(id),
|
||||
readSettings: readGitFetchSettings,
|
||||
// The same Docker list the panel's routes use: the rule has to be one rule.
|
||||
// The same Docker list the panel's Fetch button uses: the rule has to be one rule. It rejects on a list
|
||||
// that cannot be read, and the tick then fetches nothing (fail closed).
|
||||
dockerWorkspaces: defaultDockerWorkspaces,
|
||||
// Bounded probe, DEFAULT options (rule #516): a tick is a bulk scan, so it keeps the stall cap and must
|
||||
// never be handed `pastCap`. A folder on a mount that stopped answering is skipped, not fetched.
|
||||
|
||||
Reference in new issue
Block a user