@evo-dev/core 0.0.1-alpha
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/assets/agents/review/code-reviewer/examples.md +19 -0
- package/assets/agents/review/code-reviewer/manifest.json +10 -0
- package/assets/agents/review/code-reviewer/prompt.md +59 -0
- package/assets/agents/review/code-reviewer/verification.md +11 -0
- package/assets/skills/coding/engineering-discipline/SKILL.md +63 -0
- package/assets/skills/coding/engineering-discipline/anti-patterns.md +21 -0
- package/assets/skills/coding/engineering-discipline/examples.md +19 -0
- package/assets/skills/coding/engineering-discipline/manifest.json +10 -0
- package/assets/skills/coding/engineering-discipline/verification.md +11 -0
- package/assets/workflows/rd-bug-fix/WORKFLOW.json +45 -0
- package/assets/workflows/rd-code-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-docs-update/WORKFLOW.json +45 -0
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +45 -0
- package/assets/workflows/rd-refactor/WORKFLOW.json +45 -0
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +49 -0
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-test-generation/WORKFLOW.json +45 -0
- package/dist/assets/index.js +209 -0
- package/dist/config/index.js +601 -0
- package/dist/index.js +4879 -0
- package/dist/plugins/index.js +265 -0
- package/package.json +30 -0
- package/src/.gitkeep +0 -0
- package/src/agents/index.ts +561 -0
- package/src/assets/errors.ts +21 -0
- package/src/assets/index.ts +18 -0
- package/src/assets/manifest.ts +109 -0
- package/src/assets/scanner.ts +189 -0
- package/src/config/errors.ts +21 -0
- package/src/config/index.ts +26 -0
- package/src/config/paths.ts +43 -0
- package/src/config/registry.ts +84 -0
- package/src/config/settings.ts +212 -0
- package/src/config/state.ts +130 -0
- package/src/config/store.ts +166 -0
- package/src/daemon/index.ts +414 -0
- package/src/hooks/index.ts +1023 -0
- package/src/index.ts +14 -0
- package/src/learning/index.ts +714 -0
- package/src/observability/index.ts +272 -0
- package/src/pack/index.ts +779 -0
- package/src/plugins/capabilities.ts +347 -0
- package/src/plugins/index.ts +41 -0
- package/src/plugins/registry.ts +60 -0
- package/src/plugins/types.ts +123 -0
- package/src/project/index.ts +507 -0
- package/src/protected-zones/index.ts +137 -0
- package/src/sync/index.ts +7 -0
- package/src/sync/orchestrator.ts +298 -0
- package/src/task/index.ts +840 -0
- package/src/workflow/index.ts +137 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { EvoDevConfigError } from "./errors.ts";
|
|
4
|
+
import { type EvoDevPaths, resolveEvoDevPaths } from "./paths.ts";
|
|
5
|
+
import { type EvoDevRegistry, createDefaultRegistry, parseRegistry } from "./registry.ts";
|
|
6
|
+
import {
|
|
7
|
+
type EvoDevSettings,
|
|
8
|
+
type SettingsInput,
|
|
9
|
+
createDefaultSettings,
|
|
10
|
+
mergeSettings,
|
|
11
|
+
parseSettings,
|
|
12
|
+
} from "./settings.ts";
|
|
13
|
+
import {
|
|
14
|
+
type InstallState,
|
|
15
|
+
type SyncState,
|
|
16
|
+
createDefaultInstallState,
|
|
17
|
+
createDefaultSyncState,
|
|
18
|
+
parseInstallState,
|
|
19
|
+
parseSyncState,
|
|
20
|
+
} from "./state.ts";
|
|
21
|
+
|
|
22
|
+
export interface CoreConfigStore {
|
|
23
|
+
readonly paths: EvoDevPaths;
|
|
24
|
+
ensureBaseDirs(): Promise<void>;
|
|
25
|
+
readSettings(): Promise<EvoDevSettings>;
|
|
26
|
+
writeSettings(settings: EvoDevSettings): Promise<void>;
|
|
27
|
+
mergeAndWriteSettings(input: SettingsInput): Promise<EvoDevSettings>;
|
|
28
|
+
readRegistry(): Promise<EvoDevRegistry>;
|
|
29
|
+
writeRegistry(registry: EvoDevRegistry): Promise<void>;
|
|
30
|
+
readInstallState(): Promise<InstallState>;
|
|
31
|
+
writeInstallState(state: InstallState): Promise<void>;
|
|
32
|
+
readSyncState(): Promise<SyncState>;
|
|
33
|
+
writeSyncState(state: SyncState): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createCoreConfigStore(homeDir?: string): CoreConfigStore {
|
|
37
|
+
const paths = resolveEvoDevPaths(homeDir);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
paths,
|
|
41
|
+
async ensureBaseDirs() {
|
|
42
|
+
await mkdir(paths.stateDir, { recursive: true });
|
|
43
|
+
},
|
|
44
|
+
async readSettings() {
|
|
45
|
+
return readJsonFile(paths.settingsPath, parseSettings);
|
|
46
|
+
},
|
|
47
|
+
async writeSettings(settings) {
|
|
48
|
+
await writeJsonFile(paths.settingsPath, parseSettings(settings));
|
|
49
|
+
},
|
|
50
|
+
async mergeAndWriteSettings(input) {
|
|
51
|
+
const current = await readJsonFileOrDefault(
|
|
52
|
+
paths.settingsPath,
|
|
53
|
+
parseSettings,
|
|
54
|
+
createDefaultSettings(),
|
|
55
|
+
);
|
|
56
|
+
const merged = mergeSettings(input, current);
|
|
57
|
+
await writeJsonFile(paths.settingsPath, merged);
|
|
58
|
+
return merged;
|
|
59
|
+
},
|
|
60
|
+
async readRegistry() {
|
|
61
|
+
return readJsonFile(paths.registryPath, parseRegistry);
|
|
62
|
+
},
|
|
63
|
+
async writeRegistry(registry) {
|
|
64
|
+
await writeJsonFile(paths.registryPath, parseRegistry(registry));
|
|
65
|
+
},
|
|
66
|
+
async readInstallState() {
|
|
67
|
+
return readJsonFile(paths.installStatePath, parseInstallState);
|
|
68
|
+
},
|
|
69
|
+
async writeInstallState(state) {
|
|
70
|
+
await writeJsonFile(paths.installStatePath, parseInstallState(state));
|
|
71
|
+
},
|
|
72
|
+
async readSyncState() {
|
|
73
|
+
return readJsonFile(paths.syncStatePath, parseSyncState);
|
|
74
|
+
},
|
|
75
|
+
async writeSyncState(state) {
|
|
76
|
+
await writeJsonFile(paths.syncStatePath, parseSyncState(state));
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function initializeCoreConfig(homeDir?: string): Promise<CoreConfigStore> {
|
|
82
|
+
const store = createCoreConfigStore(homeDir);
|
|
83
|
+
await store.ensureBaseDirs();
|
|
84
|
+
await writeIfMissing(store.paths.settingsPath, createDefaultSettings());
|
|
85
|
+
await writeIfMissing(store.paths.registryPath, createDefaultRegistry());
|
|
86
|
+
await writeIfMissing(store.paths.installStatePath, createDefaultInstallState());
|
|
87
|
+
await writeIfMissing(store.paths.syncStatePath, createDefaultSyncState());
|
|
88
|
+
return store;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readJsonFile<T>(filePath: string, parse: (value: unknown) => T): Promise<T> {
|
|
92
|
+
let raw: string;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
raw = await readFile(filePath, "utf8");
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new EvoDevConfigError(`Cannot read config file (${describeFileError(error)})`, filePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let json: unknown;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
json = JSON.parse(raw);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new EvoDevConfigError(`Invalid JSON (${describeFileError(error)})`, filePath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return parse(json);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof EvoDevConfigError) {
|
|
112
|
+
throw new EvoDevConfigError(error.message, filePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function readJsonFileOrDefault<T>(
|
|
120
|
+
filePath: string,
|
|
121
|
+
parse: (value: unknown) => T,
|
|
122
|
+
fallback: T,
|
|
123
|
+
): Promise<T> {
|
|
124
|
+
try {
|
|
125
|
+
return await readJsonFile(filePath, parse);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof EvoDevConfigError && error.message.includes("ENOENT")) {
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function writeIfMissing(filePath: string, value: unknown): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
await readFile(filePath, "utf8");
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
140
|
+
await writeJsonFile(filePath, value);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new EvoDevConfigError(
|
|
145
|
+
`Cannot inspect config file (${describeFileError(error)})`,
|
|
146
|
+
filePath,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
152
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
153
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function describeFileError(error: unknown): string {
|
|
157
|
+
if (error instanceof Error) {
|
|
158
|
+
return error.message;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return String(error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
165
|
+
return error instanceof Error && "code" in error;
|
|
166
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { type IncomingMessage, createServer } from "node:http";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { listObservabilityEvents } from "../observability/index.ts";
|
|
6
|
+
|
|
7
|
+
export interface DaemonPaths {
|
|
8
|
+
rootDir: string;
|
|
9
|
+
lockPath: string;
|
|
10
|
+
tokenPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DaemonLock {
|
|
14
|
+
version: 1;
|
|
15
|
+
component: "evodev-daemon";
|
|
16
|
+
pid: number;
|
|
17
|
+
host: "127.0.0.1" | "localhost";
|
|
18
|
+
port: number;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
heartbeatAt: string;
|
|
21
|
+
status: "running";
|
|
22
|
+
tokenPath: string;
|
|
23
|
+
versionText: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DaemonStartPlan {
|
|
27
|
+
host: "127.0.0.1" | "localhost";
|
|
28
|
+
port: number;
|
|
29
|
+
paths: DaemonPaths;
|
|
30
|
+
writes: string[];
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DaemonResponseBody {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
data: unknown;
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DaemonRequestInput {
|
|
41
|
+
method: string;
|
|
42
|
+
path: string;
|
|
43
|
+
token?: string | null;
|
|
44
|
+
homeDir: string;
|
|
45
|
+
origin?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_PORT = 37645;
|
|
49
|
+
const SENSITIVE_TEXT_PATTERN =
|
|
50
|
+
/https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw output|raw source|raw prompt|transcript|stdout|stderr)\b/i;
|
|
51
|
+
|
|
52
|
+
export function resolveDaemonPaths(homeDir: string): DaemonPaths {
|
|
53
|
+
const rootDir = join(homeDir, ".evodev", "STATE", "daemon");
|
|
54
|
+
return { rootDir, lockPath: join(rootDir, "lock.json"), tokenPath: join(rootDir, "token") };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateDaemonBindHost(host: string): "127.0.0.1" | "localhost" {
|
|
58
|
+
if (host === "127.0.0.1" || host === "localhost") return host;
|
|
59
|
+
throw new Error("Daemon bind host must be local-only: 127.0.0.1 or localhost.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createDaemonStartPlan(input: {
|
|
63
|
+
homeDir: string;
|
|
64
|
+
host?: string;
|
|
65
|
+
port?: number;
|
|
66
|
+
}): DaemonStartPlan {
|
|
67
|
+
const host = validateDaemonBindHost(input.host ?? "127.0.0.1");
|
|
68
|
+
const port = input.port ?? DEFAULT_PORT;
|
|
69
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("Invalid daemon port.");
|
|
70
|
+
const paths = resolveDaemonPaths(input.homeDir);
|
|
71
|
+
return {
|
|
72
|
+
host,
|
|
73
|
+
port,
|
|
74
|
+
paths,
|
|
75
|
+
writes: [paths.lockPath, paths.tokenPath],
|
|
76
|
+
warnings: ["Daemon is disabled by default and starts only via explicit daemon start."],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function writeDaemonState(input: {
|
|
81
|
+
homeDir: string;
|
|
82
|
+
host?: string;
|
|
83
|
+
port?: number;
|
|
84
|
+
now?: string;
|
|
85
|
+
pid?: number;
|
|
86
|
+
versionText?: string;
|
|
87
|
+
}): Promise<{ lock: DaemonLock; token: string; paths: DaemonPaths }> {
|
|
88
|
+
const plan = createDaemonStartPlan(input);
|
|
89
|
+
if ((await readDaemonLock(input.homeDir)) !== null) {
|
|
90
|
+
throw new Error("Existing daemon lock found; refusing to overwrite daemon state.");
|
|
91
|
+
}
|
|
92
|
+
const token = createDaemonToken();
|
|
93
|
+
const now = input.now ?? new Date().toISOString();
|
|
94
|
+
const lock: DaemonLock = {
|
|
95
|
+
version: 1,
|
|
96
|
+
component: "evodev-daemon",
|
|
97
|
+
pid: input.pid ?? process.pid,
|
|
98
|
+
host: plan.host,
|
|
99
|
+
port: plan.port,
|
|
100
|
+
startedAt: now,
|
|
101
|
+
heartbeatAt: now,
|
|
102
|
+
status: "running",
|
|
103
|
+
tokenPath: plan.paths.tokenPath,
|
|
104
|
+
versionText: input.versionText ?? "evodev 0.0.1-alpha",
|
|
105
|
+
};
|
|
106
|
+
await mkdir(dirname(plan.paths.lockPath), { recursive: true });
|
|
107
|
+
await writeFile(plan.paths.tokenPath, `${token}\n`, {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
flag: "wx",
|
|
110
|
+
mode: 0o600,
|
|
111
|
+
});
|
|
112
|
+
await writeFile(plan.paths.lockPath, `${JSON.stringify(lock, null, 2)}\n`, {
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
flag: "wx",
|
|
115
|
+
mode: 0o600,
|
|
116
|
+
});
|
|
117
|
+
return { lock, token, paths: plan.paths };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readDaemonLock(homeDir: string): Promise<DaemonLock | null> {
|
|
121
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
122
|
+
if (!(await pathExists(paths.lockPath))) return null;
|
|
123
|
+
const lock = JSON.parse(await readFile(paths.lockPath, "utf8")) as DaemonLock;
|
|
124
|
+
if (lock.version !== 1 || lock.component !== "evodev-daemon")
|
|
125
|
+
throw new Error("Invalid daemon lock.");
|
|
126
|
+
return lock;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function readDaemonToken(homeDir: string): Promise<string | null> {
|
|
130
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
131
|
+
if (!(await pathExists(paths.tokenPath))) return null;
|
|
132
|
+
return (await readFile(paths.tokenPath, "utf8")).trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function cleanupDaemonState(homeDir: string, token: string): Promise<string[]> {
|
|
136
|
+
const paths = resolveDaemonPaths(homeDir);
|
|
137
|
+
const currentToken = await readDaemonToken(homeDir);
|
|
138
|
+
if (currentToken === null || currentToken !== token) throw new Error("Invalid daemon token.");
|
|
139
|
+
const lock = await readDaemonLock(homeDir);
|
|
140
|
+
if (lock === null || lock.component !== "evodev-daemon")
|
|
141
|
+
throw new Error("Missing valid daemon lock.");
|
|
142
|
+
await rm(paths.lockPath, { force: true });
|
|
143
|
+
await rm(paths.tokenPath, { force: true });
|
|
144
|
+
return [paths.lockPath, paths.tokenPath];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function handleDaemonRequest(
|
|
148
|
+
input: DaemonRequestInput,
|
|
149
|
+
): Promise<{ status: number; body: DaemonResponseBody }> {
|
|
150
|
+
const warnings: string[] = [];
|
|
151
|
+
if (!isAllowedLocalOrigin(input.origin ?? null)) {
|
|
152
|
+
return { status: 403, body: { ok: false, data: { error: "forbidden origin" }, warnings } };
|
|
153
|
+
}
|
|
154
|
+
if (input.path === "/health" && input.method === "GET") {
|
|
155
|
+
return ok({ status: "ok", version: 1, metadataOnly: true }, warnings);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const auth = await authorize(input.homeDir, input.token ?? null);
|
|
159
|
+
if (!auth.ok)
|
|
160
|
+
return { status: 401, body: { ok: false, data: { error: "unauthorized" }, warnings } };
|
|
161
|
+
|
|
162
|
+
if (input.path === "/shutdown" && input.method === "POST") {
|
|
163
|
+
const removed = await cleanupDaemonState(input.homeDir, input.token ?? "");
|
|
164
|
+
return ok({ stopped: true, removed }, warnings);
|
|
165
|
+
}
|
|
166
|
+
if (input.method !== "GET") return notFound(warnings);
|
|
167
|
+
|
|
168
|
+
if (input.path === "/tasks")
|
|
169
|
+
return ok(await collectTaskSummaries(input.homeDir, warnings), warnings);
|
|
170
|
+
if (input.path === "/observability/events")
|
|
171
|
+
return ok(await collectObservabilitySummaries(input.homeDir, warnings), warnings);
|
|
172
|
+
if (input.path === "/memory/candidates")
|
|
173
|
+
return ok(await collectLearningCandidateSummaries(input.homeDir, warnings), warnings);
|
|
174
|
+
if (input.path === "/projects")
|
|
175
|
+
return ok(
|
|
176
|
+
await collectDirectorySummaries(join(input.homeDir, ".evodev", "PROJECTS"), warnings),
|
|
177
|
+
warnings,
|
|
178
|
+
);
|
|
179
|
+
if (input.path === "/packs")
|
|
180
|
+
return ok(
|
|
181
|
+
await collectDirectorySummaries(join(input.homeDir, ".evodev", "PACKS"), warnings),
|
|
182
|
+
warnings,
|
|
183
|
+
);
|
|
184
|
+
if (input.path === "/runs") return ok([], warnings);
|
|
185
|
+
if (input.path === "/agents") return ok([], warnings);
|
|
186
|
+
|
|
187
|
+
return notFound(warnings);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function runDaemonForeground(input: {
|
|
191
|
+
homeDir: string;
|
|
192
|
+
host?: string;
|
|
193
|
+
port?: number;
|
|
194
|
+
write?: (message: string) => void;
|
|
195
|
+
}): Promise<void> {
|
|
196
|
+
const state = await writeDaemonState({
|
|
197
|
+
homeDir: input.homeDir,
|
|
198
|
+
host: input.host,
|
|
199
|
+
port: input.port,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const server = createServer(async (request, response) => {
|
|
203
|
+
try {
|
|
204
|
+
const url = new URL(request.url ?? "/", `http://${state.lock.host}:${state.lock.port}`);
|
|
205
|
+
const result = await handleDaemonRequest({
|
|
206
|
+
method: request.method ?? "GET",
|
|
207
|
+
path: url.pathname,
|
|
208
|
+
token:
|
|
209
|
+
readHeader(request, "authorization")?.replace(/^Bearer\s+/i, "") ??
|
|
210
|
+
readHeader(request, "x-evodev-token") ??
|
|
211
|
+
url.searchParams.get("token"),
|
|
212
|
+
homeDir: input.homeDir,
|
|
213
|
+
origin: readHeader(request, "origin"),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
response.writeHead(result.status, { "content-type": "application/json" });
|
|
217
|
+
response.end(JSON.stringify(result.body));
|
|
218
|
+
|
|
219
|
+
if (url.pathname === "/shutdown" && result.status === 200) {
|
|
220
|
+
server.close();
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
response.writeHead(500, { "content-type": "application/json" });
|
|
224
|
+
response.end(
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
ok: false,
|
|
227
|
+
data: { error: error instanceof Error ? error.message : String(error) },
|
|
228
|
+
warnings: [],
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await new Promise<void>((resolve, reject) => {
|
|
235
|
+
const onError = (error: Error) => {
|
|
236
|
+
server.off("listening", onListening);
|
|
237
|
+
reject(error);
|
|
238
|
+
};
|
|
239
|
+
const onListening = () => {
|
|
240
|
+
server.off("error", onError);
|
|
241
|
+
resolve();
|
|
242
|
+
};
|
|
243
|
+
server.once("error", onError);
|
|
244
|
+
server.once("listening", onListening);
|
|
245
|
+
server.listen(state.lock.port, state.lock.host);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
input.write?.(
|
|
249
|
+
`Daemon listening on ${state.lock.host}:${state.lock.port}; token path: ${state.paths.tokenPath}`,
|
|
250
|
+
);
|
|
251
|
+
await new Promise<void>((resolve, reject) => {
|
|
252
|
+
server.once("close", resolve);
|
|
253
|
+
server.once("error", reject);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readHeader(request: IncomingMessage, name: string): string | null {
|
|
258
|
+
const value = request.headers[name.toLowerCase()];
|
|
259
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
260
|
+
return value ?? null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createDaemonToken(): string {
|
|
264
|
+
return randomBytes(32).toString("hex");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function authorize(homeDir: string, token: string | null): Promise<{ ok: boolean }> {
|
|
268
|
+
const expected = await readDaemonToken(homeDir);
|
|
269
|
+
return { ok: expected !== null && token !== null && token === expected };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isAllowedLocalOrigin(origin: string | null): boolean {
|
|
273
|
+
if (origin === null || origin === "") return true;
|
|
274
|
+
try {
|
|
275
|
+
const parsed = new URL(origin);
|
|
276
|
+
return (
|
|
277
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
278
|
+
(parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost")
|
|
279
|
+
);
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function collectTaskSummaries(homeDir: string, warnings: string[]): Promise<unknown[]> {
|
|
286
|
+
const root = join(homeDir, ".evodev", "STATE", "tasks");
|
|
287
|
+
if (!(await pathExists(root))) {
|
|
288
|
+
warnings.push("Task store not found; returning empty tasks.");
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const contracts = await collectNamedFiles(root, "contract.json");
|
|
292
|
+
const summaries: unknown[] = [];
|
|
293
|
+
for (const file of contracts) {
|
|
294
|
+
try {
|
|
295
|
+
const contract = JSON.parse(await readFile(file, "utf8"));
|
|
296
|
+
summaries.push(
|
|
297
|
+
sanitizeMetadata({
|
|
298
|
+
taskId: contract.taskId,
|
|
299
|
+
status: contract.status,
|
|
300
|
+
mode: contract.route?.mode ?? null,
|
|
301
|
+
workflowId: contract.route?.workflowId ?? null,
|
|
302
|
+
verificationStatus: contract.verification?.status ?? null,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
} catch {
|
|
306
|
+
warnings.push(`Skipped unreadable task contract: ${file}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return summaries;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function collectObservabilitySummaries(
|
|
313
|
+
homeDir: string,
|
|
314
|
+
warnings: string[],
|
|
315
|
+
): Promise<unknown[]> {
|
|
316
|
+
try {
|
|
317
|
+
return (await listObservabilityEvents(homeDir)).map((event) =>
|
|
318
|
+
sanitizeMetadata({
|
|
319
|
+
eventId: event.eventId,
|
|
320
|
+
type: event.type,
|
|
321
|
+
timestamp: event.timestamp,
|
|
322
|
+
summary: event.summary,
|
|
323
|
+
scope: event.scope ?? {},
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
warnings.push(
|
|
328
|
+
`Observability unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
+
);
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function collectLearningCandidateSummaries(
|
|
335
|
+
homeDir: string,
|
|
336
|
+
warnings: string[],
|
|
337
|
+
): Promise<unknown[]> {
|
|
338
|
+
const path = join(homeDir, ".evodev", "STATE", "learning", "candidates.jsonl");
|
|
339
|
+
if (!(await pathExists(path))) {
|
|
340
|
+
warnings.push("Learning candidate store not found; returning empty candidates.");
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const lines = (await readFile(path, "utf8")).split("\n").filter(Boolean);
|
|
344
|
+
return lines.map((line) => {
|
|
345
|
+
const candidate = JSON.parse(line);
|
|
346
|
+
return sanitizeMetadata({
|
|
347
|
+
id: candidate.id,
|
|
348
|
+
kind: candidate.kind,
|
|
349
|
+
status: candidate.status,
|
|
350
|
+
scope: candidate.scope,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function collectDirectorySummaries(root: string, warnings: string[]): Promise<unknown[]> {
|
|
356
|
+
if (!(await pathExists(root))) {
|
|
357
|
+
warnings.push(`Store not found: ${root}`);
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
361
|
+
return entries
|
|
362
|
+
.filter((entry) => entry.isDirectory())
|
|
363
|
+
.map((entry) => ({ id: entry.name, metadataOnly: true }));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function sanitizeMetadata(value: unknown): unknown {
|
|
367
|
+
if (typeof value === "string") {
|
|
368
|
+
if (SENSITIVE_TEXT_PATTERN.test(value)) return "[redacted]";
|
|
369
|
+
return value.slice(0, 500);
|
|
370
|
+
}
|
|
371
|
+
if (Array.isArray(value)) return value.map(sanitizeMetadata);
|
|
372
|
+
if (typeof value !== "object" || value === null) return value;
|
|
373
|
+
const output: Record<string, unknown> = {};
|
|
374
|
+
for (const [key, child] of Object.entries(value)) {
|
|
375
|
+
if (/raw|prompt|source|stdout|stderr|secret|token|password|transcript|memorybody/i.test(key))
|
|
376
|
+
continue;
|
|
377
|
+
output[key] = sanitizeMetadata(child);
|
|
378
|
+
}
|
|
379
|
+
return output;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function collectNamedFiles(root: string, name: string): Promise<string[]> {
|
|
383
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
384
|
+
const files: string[] = [];
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
const path = join(root, entry.name);
|
|
387
|
+
if (entry.isDirectory()) files.push(...(await collectNamedFiles(path, name)));
|
|
388
|
+
else if (entry.isFile() && entry.name === name) files.push(path);
|
|
389
|
+
}
|
|
390
|
+
return files;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ok(data: unknown, warnings: string[]): { status: number; body: DaemonResponseBody } {
|
|
394
|
+
return { status: 200, body: { ok: true, data: sanitizeMetadata(data), warnings } };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function notFound(warnings: string[]): { status: number; body: DaemonResponseBody } {
|
|
398
|
+
return { status: 404, body: { ok: false, data: { error: "not found" }, warnings } };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
402
|
+
try {
|
|
403
|
+
await stat(path);
|
|
404
|
+
return true;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (
|
|
407
|
+
error instanceof Error &&
|
|
408
|
+
"code" in error &&
|
|
409
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
410
|
+
)
|
|
411
|
+
return false;
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|