Address remaining Greptile portability feedback

This commit is contained in:
dotta
2026-03-20 08:55:10 -05:00
parent 581a654748
commit 79652da520
5 changed files with 131 additions and 9 deletions

View File

@@ -623,6 +623,63 @@ describe("company portability", () => {
]);
});
it("treats no-separator auth and api key env names as secrets during export", async () => {
const portability = companyPortabilityService({} as any);
agentSvc.list.mockResolvedValue([
{
id: "agent-1",
name: "ClaudeCoder",
status: "idle",
role: "engineer",
title: "Software Engineer",
icon: "code",
reportsTo: null,
capabilities: "Writes code",
adapterType: "claude_local",
adapterConfig: {
promptTemplate: "You are ClaudeCoder.",
env: {
APIKEY: {
type: "plain",
value: "sk-plain-api",
},
GITHUBAUTH: {
type: "plain",
value: "gh-auth-token",
},
PRIVATEKEY: {
type: "plain",
value: "private-key-value",
},
},
},
runtimeConfig: {},
budgetMonthlyCents: 0,
permissions: {},
metadata: null,
},
]);
const exported = await portability.exportBundle("company-1", {
include: {
company: true,
agents: true,
projects: false,
issues: false,
},
});
const extension = asTextFile(exported.files[".paperclip.yaml"]);
expect(extension).toContain("APIKEY:");
expect(extension).toContain("GITHUBAUTH:");
expect(extension).toContain("PRIVATEKEY:");
expect(extension).not.toContain("sk-plain-api");
expect(extension).not.toContain("gh-auth-token");
expect(extension).not.toContain("private-key-value");
expect(extension).toContain('kind: "secret"');
});
it("imports packaged skills and restores desired skill refs on agents", async () => {
const portability = companyPortabilityService({} as any);

View File

@@ -138,6 +138,45 @@ describe("project workspace skill discovery", () => {
expect(imported.fileInventory.map((entry) => entry.kind)).toContain("script");
expect(imported.metadata?.sourceKind).toBe("project_scan");
});
it("parses inline object array items in skill frontmatter metadata", async () => {
const workspace = await makeTempDir("paperclip-inline-skill-yaml-");
await fs.mkdir(workspace, { recursive: true });
await fs.writeFile(
path.join(workspace, "SKILL.md"),
[
"---",
"name: Inline Metadata Skill",
"metadata:",
" sources:",
" - kind: github-dir",
" repo: paperclipai/paperclip",
" path: skills/paperclip",
"---",
"",
"# Inline Metadata Skill",
"",
].join("\n"),
"utf8",
);
const imported = await readLocalSkillImportFromDirectory(
"33333333-3333-4333-8333-333333333333",
workspace,
{ inventoryMode: "full" },
);
expect(imported.metadata).toMatchObject({
sourceKind: "local_path",
sources: [
{
kind: "github-dir",
repo: "paperclipai/paperclip",
path: "skills/paperclip",
},
],
});
});
});
describe("missing local skill reconciliation", () => {

View File

@@ -351,10 +351,12 @@ function isSensitiveEnvKey(key: string) {
normalized === "token" ||
normalized.endsWith("_token") ||
normalized.endsWith("-token") ||
normalized.includes("apikey") ||
normalized.includes("api_key") ||
normalized.includes("api-key") ||
normalized.includes("access_token") ||
normalized.includes("access-token") ||
normalized.includes("auth") ||
normalized.includes("auth_token") ||
normalized.includes("auth-token") ||
normalized.includes("authorization") ||
@@ -364,6 +366,7 @@ function isSensitiveEnvKey(key: string) {
normalized.includes("password") ||
normalized.includes("credential") ||
normalized.includes("jwt") ||
normalized.includes("privatekey") ||
normalized.includes("private_key") ||
normalized.includes("private-key") ||
normalized.includes("cookie") ||

View File

@@ -377,6 +377,28 @@ function parseYamlBlock(
index = nested.nextIndex;
continue;
}
const inlineObjectSeparator = remainder.indexOf(":");
if (
inlineObjectSeparator > 0 &&
!remainder.startsWith("\"") &&
!remainder.startsWith("{") &&
!remainder.startsWith("[")
) {
const key = remainder.slice(0, inlineObjectSeparator).trim();
const rawValue = remainder.slice(inlineObjectSeparator + 1).trim();
const nextObject: Record<string, unknown> = {
[key]: parseYamlScalar(rawValue),
};
if (index < lines.length && lines[index]!.indent > indentLevel) {
const nested = parseYamlBlock(lines, index, indentLevel + 2);
if (isPlainRecord(nested.value)) {
Object.assign(nextObject, nested.value);
}
index = nested.nextIndex;
}
values.push(nextObject);
continue;
}
values.push(parseYamlScalar(remainder));
}
return { value: values, nextIndex: index };
@@ -804,12 +826,11 @@ export async function readLocalSkillImportFromDirectory(
const markdown = await fs.readFile(skillFilePath, "utf8");
const parsed = parseFrontmatterMarkdown(markdown);
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(resolvedSkillDir));
const skillKey = readCanonicalSkillKey(
parsed.frontmatter,
isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null,
);
const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null;
const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata);
const metadata = {
...(skillKey ? { skillKey } : {}),
...(parsedMetadata ?? {}),
sourceKind: "local_path",
...(options?.metadata ?? {}),
};
@@ -877,12 +898,11 @@ async function readLocalSkillImports(companyId: string, sourcePath: string): Pro
const markdown = await fs.readFile(resolvedPath, "utf8");
const parsed = parseFrontmatterMarkdown(markdown);
const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(path.dirname(resolvedPath)));
const skillKey = readCanonicalSkillKey(
parsed.frontmatter,
isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null,
);
const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null;
const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata);
const metadata = {
...(skillKey ? { skillKey } : {}),
...(parsedMetadata ?? {}),
sourceKind: "local_path",
};
const inventory: CompanySkillFileInventoryEntry[] = [