@sema-agent/core
Version:
Stateless, task-oriented AI agent core
211 lines (210 loc) • 8.57 kB
JavaScript
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 };
}