@john-ezra/openralph 0.1.1

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.
@@ -0,0 +1,143 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import { join, normalize } from "node:path"
4
+ import { pathToFileURL } from "node:url"
5
+ import { runCommand } from "./exec.ts"
6
+
7
+ export interface ReleaseCheckFinding {
8
+ file: string
9
+ line: number
10
+ reason: string
11
+ marker: string
12
+ }
13
+
14
+ export interface ForbiddenPathMarker {
15
+ reason: string
16
+ value: string
17
+ }
18
+
19
+ export interface ReleaseCheckResult {
20
+ files: string[]
21
+ findings: ReleaseCheckFinding[]
22
+ }
23
+
24
+ interface NpmPackFile {
25
+ path?: string
26
+ }
27
+
28
+ interface NpmPackEntry {
29
+ files?: NpmPackFile[]
30
+ }
31
+
32
+ export async function runReleaseCheck(cwd = process.cwd()): Promise<ReleaseCheckResult> {
33
+ const [trackedFiles, packedFiles] = await Promise.all([listGitTrackedFiles(cwd), listNpmPackFiles(cwd)])
34
+ const files = uniqueSorted([...trackedFiles, ...packedFiles])
35
+ const findings = await scanFilesForForbiddenPaths(cwd, files, buildForbiddenPathMarkers(cwd))
36
+ return { files, findings }
37
+ }
38
+
39
+ export function buildForbiddenPathMarkers(projectRoot: string, home = homedir()): ForbiddenPathMarker[] {
40
+ const markers: ForbiddenPathMarker[] = []
41
+ const seen = new Set<string>()
42
+
43
+ addMarker(markers, seen, pathToFileURL(normalizePath(home)).href, "file URL pointing into the current user's home directory")
44
+ addMarker(markers, seen, normalizePath(home), "absolute path under the current user's home directory")
45
+ addMarker(markers, seen, normalizePath(projectRoot), "absolute path into this local OpenRalph checkout")
46
+
47
+ return markers
48
+ }
49
+
50
+ export async function scanFilesForForbiddenPaths(
51
+ cwd: string,
52
+ files: string[],
53
+ markers = buildForbiddenPathMarkers(cwd),
54
+ ): Promise<ReleaseCheckFinding[]> {
55
+ const findings: ReleaseCheckFinding[] = []
56
+
57
+ for (const file of files) {
58
+ let content: Buffer
59
+ try {
60
+ content = await readFile(join(cwd, file))
61
+ } catch (error) {
62
+ if (isMissingFileError(error)) continue
63
+ throw error
64
+ }
65
+
66
+ if (content.includes(0)) continue
67
+
68
+ const lines = content.toString("utf8").split(/\r?\n/)
69
+ for (let index = 0; index < lines.length; index += 1) {
70
+ for (const marker of markers) {
71
+ if (lines[index].includes(marker.value)) {
72
+ findings.push({ file, line: index + 1, reason: marker.reason, marker: marker.value })
73
+ break
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ return findings
80
+ }
81
+
82
+ function addMarker(markers: ForbiddenPathMarker[], seen: Set<string>, value: string, reason: string): void {
83
+ if (!value || value === "/" || seen.has(value)) return
84
+ seen.add(value)
85
+ markers.push({ value, reason })
86
+ }
87
+
88
+ function normalizePath(path: string): string {
89
+ return normalize(path).replace(/\/$/, "")
90
+ }
91
+
92
+ function isMissingFileError(error: unknown): boolean {
93
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
94
+ }
95
+
96
+ async function listGitTrackedFiles(cwd: string): Promise<string[]> {
97
+ const result = await runCommand("git", ["ls-files", "-z"], cwd)
98
+ if (result.exitCode !== 0) throw new Error(`git ls-files failed: ${result.stderr.trim() || "unknown error"}`)
99
+ return result.stdout.split("\0").filter(Boolean)
100
+ }
101
+
102
+ async function listNpmPackFiles(cwd: string): Promise<string[]> {
103
+ const result = await runCommand("npm", ["pack", "--dry-run", "--json"], cwd)
104
+ if (result.exitCode !== 0) throw new Error(`npm pack --dry-run failed: ${result.stderr.trim() || "unknown error"}`)
105
+
106
+ let entries: NpmPackEntry[]
107
+ try {
108
+ entries = JSON.parse(result.stdout.trim()) as NpmPackEntry[]
109
+ } catch (error) {
110
+ throw new Error(`npm pack --dry-run returned invalid JSON: ${formatError(error)}`)
111
+ }
112
+ return entries.flatMap((entry) => entry.files?.map((file) => file.path).filter((path): path is string => Boolean(path)) ?? [])
113
+ }
114
+
115
+ function uniqueSorted(values: string[]): string[] {
116
+ return [...new Set(values)].sort()
117
+ }
118
+
119
+ function formatFindings(findings: ReleaseCheckFinding[]): string {
120
+ return [
121
+ "Release path check failed: found local machine paths in public files.",
122
+ ...findings.map((finding) => `${finding.file}:${finding.line}: ${finding.reason}: ${finding.marker}`),
123
+ ].join("\n")
124
+ }
125
+
126
+ function formatError(error: unknown): string {
127
+ return error instanceof Error ? error.message : String(error)
128
+ }
129
+
130
+ if (import.meta.main) {
131
+ try {
132
+ const result = await runReleaseCheck()
133
+ if (result.findings.length > 0) {
134
+ process.stderr.write(`${formatFindings(result.findings)}\n`)
135
+ process.exit(1)
136
+ }
137
+
138
+ process.stdout.write(`Release path check passed: scanned ${result.files.length} public files.\n`)
139
+ } catch (error) {
140
+ process.stderr.write(`Release path check failed: ${error instanceof Error ? error.message : String(error)}\n`)
141
+ process.exit(1)
142
+ }
143
+ }
@@ -0,0 +1,23 @@
1
+ export const PLAN_COMPLETE = "RALPH_PLAN_COMPLETE"
2
+ export const ITERATION_COMPLETE = "RALPH_ITERATION_COMPLETE"
3
+ export const BUILD_COMPLETE = "RALPH_COMPLETE"
4
+ export const BUILD_BLOCKED = "RALPH_BLOCKED"
5
+
6
+ export type BuildSentinel = "complete" | "iteration-complete" | "blocked" | "none"
7
+
8
+ export function isPlanComplete(output: string): boolean {
9
+ return output.includes(PLAN_COMPLETE)
10
+ }
11
+
12
+ export function detectBuildSentinel(output: string): BuildSentinel {
13
+ let detected: BuildSentinel = "none"
14
+
15
+ for (const line of output.split(/\r?\n/)) {
16
+ const trimmed = line.trim()
17
+ if (trimmed === BUILD_COMPLETE) detected = "complete"
18
+ else if (trimmed === ITERATION_COMPLETE) detected = "iteration-complete"
19
+ else if (trimmed === BUILD_BLOCKED) detected = "blocked"
20
+ }
21
+
22
+ return detected
23
+ }
package/src/tags.ts ADDED
@@ -0,0 +1,22 @@
1
+ export function createBuildRunId(date = new Date()): string {
2
+ return createTimestampId(date)
3
+ }
4
+
5
+ export function createTimestampId(date = new Date()): string {
6
+ const year = date.getFullYear()
7
+ const month = pad(date.getMonth() + 1)
8
+ const day = pad(date.getDate())
9
+ const hour = pad(date.getHours())
10
+ const minute = pad(date.getMinutes())
11
+ const second = pad(date.getSeconds())
12
+ return `${year}${month}${day}-${hour}${minute}${second}`
13
+ }
14
+
15
+ export function createBuildTagName(runId: string, index: number): string {
16
+ if (!Number.isInteger(index) || index < 1) throw new Error("tag index must be a positive integer")
17
+ return `openralph/build-${runId}/${String(index).padStart(3, "0")}`
18
+ }
19
+
20
+ function pad(value: number): string {
21
+ return String(value).padStart(2, "0")
22
+ }
package/src/trust.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { randomBytes } from "node:crypto"
2
+ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import { isAbsolute, join, relative } from "node:path"
5
+
6
+ export const DOCKER_TOKEN_PATH = "/run/openralph/docker-token"
7
+
8
+ export interface TrustDeps {
9
+ containerEvidence?: () => Promise<boolean>
10
+ dockerTokenPath?: string
11
+ pathExists?: (path: string) => Promise<boolean>
12
+ readTextFile?: (path: string) => Promise<string>
13
+ }
14
+
15
+ export interface HostLoopToken {
16
+ env: Record<string, string>
17
+ cleanup: () => Promise<void>
18
+ }
19
+
20
+ export async function hasDockerMarker(env: NodeJS.ProcessEnv = process.env, deps: TrustDeps = {}): Promise<boolean> {
21
+ if (env.OPENRALPH_IN_DOCKER === "1") return true
22
+ if (env.OPENRALPH_DOCKER_TOKEN) return true
23
+ return hasContainerEvidence(deps)
24
+ }
25
+
26
+ export async function attestDockerEnvironment(env: NodeJS.ProcessEnv = process.env, deps: TrustDeps = {}): Promise<boolean> {
27
+ const token = env.OPENRALPH_DOCKER_TOKEN
28
+ if (!token || token.trim() === "") return false
29
+ if (!(await hasContainerEvidence(deps))) return false
30
+
31
+ try {
32
+ const fileToken = await readText(deps.dockerTokenPath ?? DOCKER_TOKEN_PATH, deps)
33
+ return fileToken.length > 0 && fileToken === token
34
+ } catch {
35
+ return false
36
+ }
37
+ }
38
+
39
+ export async function authorizeLoopChild(
40
+ env: NodeJS.ProcessEnv = process.env,
41
+ projectRoot?: string,
42
+ deps: TrustDeps = {},
43
+ ): Promise<boolean> {
44
+ if (env.OPENRALPH_LOOP_CHILD !== "1") return false
45
+ if (await attestDockerEnvironment(env, deps)) return true
46
+ return authorizeHostLoopChild(env, projectRoot, deps)
47
+ }
48
+
49
+ export async function authorizeHostLoopChild(
50
+ env: NodeJS.ProcessEnv = process.env,
51
+ projectRoot?: string,
52
+ deps: TrustDeps = {},
53
+ ): Promise<boolean> {
54
+ const token = env.OPENRALPH_HOST_LOOP_TOKEN
55
+ const tokenFile = env.OPENRALPH_HOST_LOOP_TOKEN_FILE
56
+ if (!token || token.trim() === "" || !tokenFile || !isAbsolute(tokenFile)) return false
57
+ if (projectRoot && isPathInside(projectRoot, tokenFile)) return false
58
+
59
+ try {
60
+ const fileToken = await readText(tokenFile, deps)
61
+ return fileToken.length > 0 && fileToken === token
62
+ } catch {
63
+ return false
64
+ }
65
+ }
66
+
67
+ export async function createHostLoopToken(): Promise<HostLoopToken> {
68
+ const tempDir = await mkdtemp(join(tmpdir(), "openralph-host-child-"))
69
+ const token = randomBytes(32).toString("hex")
70
+ const tokenFile = join(tempDir, "token")
71
+ await writeFile(tokenFile, token)
72
+
73
+ return {
74
+ env: {
75
+ OPENRALPH_HOST_LOOP_TOKEN: token,
76
+ OPENRALPH_HOST_LOOP_TOKEN_FILE: tokenFile,
77
+ },
78
+ cleanup: () => rm(tempDir, { recursive: true, force: true }),
79
+ }
80
+ }
81
+
82
+ async function hasContainerEvidence(deps: TrustDeps): Promise<boolean> {
83
+ if (deps.containerEvidence) return deps.containerEvidence()
84
+
85
+ const hasExpectedPaths = (await pathExists("/workspace", deps)) && (await pathExists("/opt/openralph", deps))
86
+ if (!hasExpectedPaths) return false
87
+
88
+ if (await pathExists("/.dockerenv", deps)) return true
89
+
90
+ try {
91
+ const cgroup = await readText("/proc/1/cgroup", deps)
92
+ return /docker|containerd|kubepods|podman/i.test(cgroup)
93
+ } catch {
94
+ return false
95
+ }
96
+ }
97
+
98
+ async function pathExists(path: string, deps: TrustDeps): Promise<boolean> {
99
+ if (deps.pathExists) return deps.pathExists(path)
100
+ try {
101
+ await access(path)
102
+ return true
103
+ } catch {
104
+ return false
105
+ }
106
+ }
107
+
108
+ async function readText(path: string, deps: TrustDeps): Promise<string> {
109
+ if (deps.readTextFile) return deps.readTextFile(path)
110
+ return readFile(path, "utf8")
111
+ }
112
+
113
+ function isPathInside(root: string, path: string): boolean {
114
+ const rel = relative(root, path)
115
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)
116
+ }