UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

211 lines (210 loc) • 8.57 kB
import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { canonicalize } from "./layout.js"; function pathSegmentsDualFamily(p) { return p.split(/[\\/]/).filter(Boolean); } const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32"; const foldPathCase = (s) => (CASE_INSENSITIVE_FS ? s.toLowerCase() : s); export const SCOPE_SEGMENT_MAX_ENCODED = 64; const SEGMENT_SAFE = /^[A-Za-z0-9._-]$/; export function encodeScopeSegment(raw) { if (raw.length === 0) throw new Error("scope segment must be non-empty"); if (!isWellFormedString(raw)) throw new Error("scope segment is not well-formed Unicode (lone surrogate)"); let out = ""; for (const ch of raw) { if (SEGMENT_SAFE.test(ch)) { out += ch; } else { for (const byte of Buffer.from(ch, "utf8")) out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`; } } if (out.length > SCOPE_SEGMENT_MAX_ENCODED) { throw new Error(`scope segment exceeds ${SCOPE_SEGMENT_MAX_ENCODED} encoded chars (${out.length}): ${out.slice(0, 24)}…`); } return out; } function isWellFormedString(s) { const anyS = s; if (typeof anyS.isWellFormed === "function") return anyS.isWellFormed(); for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (c >= 0xd800 && c <= 0xdbff) { const n = s.charCodeAt(i + 1); if (!(n >= 0xdc00 && n <= 0xdfff)) return false; i++; } else if (c >= 0xdc00 && c <= 0xdfff) return false; } return true; } const FATAL_UTF8 = new TextDecoder("utf-8", { fatal: true }); export function decodeScopeSegment(encoded) { if (encoded.length > SCOPE_SEGMENT_MAX_ENCODED) { throw new Error(`scope segment exceeds ${SCOPE_SEGMENT_MAX_ENCODED} encoded chars (${encoded.length})`); } if (!/^(?:[A-Za-z0-9._-]|%[0-9A-F]{2})*$/.test(encoded)) { throw new Error(`malformed scope segment: ${encoded}`); } const bytes = []; for (let i = 0; i < encoded.length; i++) { if (encoded[i] === "%") { bytes.push(parseInt(encoded.slice(i + 1, i + 3), 16)); i += 2; } else { bytes.push(encoded.charCodeAt(i)); } } let decoded; try { decoded = FATAL_UTF8.decode(Uint8Array.from(bytes)); } catch { throw new Error(`malformed scope segment (invalid UTF-8 in %XX sequence): ${encoded}`); } if (encodeScopeSegment(decoded) !== encoded) { throw new Error(`non-canonical scope segment (re-encode mismatch): ${encoded}`); } return decoded; } export function formatUserScope(principal) { return `user:${encodeScopeSegment(principal)}`; } export function formatOrgScope(tenant) { return `org:${encodeScopeSegment(tenant)}`; } export function formatProjScope(tenant, projectId) { return `proj:${encodeScopeSegment(tenant)}/${encodeScopeSegment(projectId)}`; } export function formatUserProjScope(principal, projectId) { return `userproj:${encodeScopeSegment(principal)}/${encodeScopeSegment(projectId)}`; } const ONE_SEGMENT = /^(user|org):((?:[A-Za-z0-9._-]|%[0-9A-F]{2})+)$/; const TWO_SEGMENT = /^(proj|userproj):((?:[A-Za-z0-9._-]|%[0-9A-F]{2})+)\/((?:[A-Za-z0-9._-]|%[0-9A-F]{2})+)$/; export function parseScopeKey(key) { const m1 = ONE_SEGMENT.exec(key); if (m1) { const seg = decodeScopeSegment(m1[2]); return m1[1] === "user" ? { kind: "user", principal: seg } : { kind: "org", tenant: seg }; } const m2 = TWO_SEGMENT.exec(key); if (m2) { const a = decodeScopeSegment(m2[2]); const b = decodeScopeSegment(m2[3]); return m2[1] === "proj" ? { kind: "proj", tenant: a, projectId: b } : { kind: "userproj", principal: a, projectId: b }; } const prefix = /^(user|org|proj|userproj):/.exec(key); if (prefix) { const e = new Error(`malformed v2 scope key "${key}": prefix "${prefix[1]}:" is reserved by the scope contract ` + `(design/142 §1.2) — segments must be encoded with encodeScopeSegment (charset [A-Za-z0-9._-] + %XX), ` + `"${prefix[1]}" takes ${prefix[1] === "proj" || prefix[1] === "userproj" ? "two segments (a/b)" : "one segment"}.`); e.code = "config.memory_scope_key"; throw e; } return { kind: "legacy", raw: key }; } export function isPersonalScope(parsed) { return parsed.kind === "user" || parsed.kind === "userproj"; } export function isCentralAuthorityScope(parsed) { return isPersonalScope(parsed) || parsed.kind === "org"; } export function assertScopeContractPlacement(args) { if (!args.repoRoot) return; const personal = args.parsedScopes.filter(isCentralAuthorityScope); if (personal.length === 0) return; const rootSegs = pathSegmentsDualFamily(canonicalize(resolve(args.repoRoot))).map(foldPathCase); const dirSegs = pathSegmentsDualFamily(canonicalize(resolve(args.memoryDir))).map(foldPathCase); if (rootSegs.length <= dirSegs.length && rootSegs.every((s, i) => s === dirSegs[i])) { const e = new Error(`memory scope contract v2: central-authority scope(s) ${personal .map((p) => `"${p.kind}"`) .join(", ")} cannot materialize into an IN-REPO memory dir (${args.memoryDir}) — ` + `a user/userproj/org layer projected under the repo root would leak non-repo memory to every ` + `collaborator (and into commit history). Move the memory dir out of the repo, or drop those scopes.`); e.code = "config.memory_scope_placement"; throw e; } } export const PROJECT_MARKER_PATH = ".sema/project"; export const PROJECT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UUID_RE = PROJECT_ID_REGEX; export function formatProjectMarker(projectId) { if (!UUID_RE.test(projectId)) throw new Error(`projectId must be a UUID, got: ${projectId}`); return `projectId = ${projectId.toLowerCase()}\n`; } export function parseProjectMarker(body) { const corrupt = (detail) => { const e = new Error(`corrupt project marker: ${detail}`); e.code = "config.memory_project_marker"; throw e; }; let projectId; for (const line of body.split(/\r?\n/)) { const t = line.trim(); if (!t || t.startsWith("#")) continue; if (!/^projectId\b/.test(t)) continue; const m = /^projectId\s*=\s*(?:"([^"]*)"|([^"\s]+))\s*$/.exec(t); if (!m) corrupt(`unparseable projectId line (${t.slice(0, 48)})`); const value = m[1] ?? m[2] ?? ""; if (!UUID_RE.test(value)) corrupt(`projectId is not a UUID (${value.slice(0, 48) || "<empty>"})`); if (projectId !== undefined && projectId !== value.toLowerCase()) corrupt("duplicate conflicting projectId lines"); projectId = value.toLowerCase(); } if (projectId === undefined) return null; return { projectId }; } function gitRootOf(dir) { let cur = resolve(dir); for (;;) { if (existsSync(join(cur, ".git"))) return cur; const parent = dirname(cur); if (parent === cur) return undefined; cur = parent; } } export function resolveProjectId(rootDir) { const dir = resolve(rootDir); const gitRoot = gitRootOf(dir); const projectRoot = gitRoot ?? dir; if (gitRoot !== undefined) { let cur = dir; while (cur !== gitRoot) { if (existsSync(join(cur, PROJECT_MARKER_PATH))) { const e = new Error(`nested project marker: ${join(cur, PROJECT_MARKER_PATH)} sits below the repo root (${gitRoot}) — ` + `v1 supports ONE marker at the repo root (design/142 §1.1); remove the nested marker or split the repo.`); e.code = "config.memory_project_marker_nested"; throw e; } const parent = dirname(cur); if (parent === cur) break; cur = parent; } } const markerFile = join(projectRoot, PROJECT_MARKER_PATH); if (!existsSync(markerFile)) return null; const parsed = parseProjectMarker(readFileSync(markerFile, "utf8")); if (!parsed) return null; return { projectId: parsed.projectId, markerDir: projectRoot }; }