feat: outil propose_asset + auto-extension exposée, fin Phase 5 v1 (v0.27.0)
Outil local sanctionné chlova.propose_asset : l'agent propose un asset → write+commit+version+doc → need-review (privilégié = BLOQUÉ). Notion ToolSpec.sanctioned (autorisé par gatekeeper, audité). Flag CHLOVA_AUTOEXT_ENABLED (off défaut) + CHLOVA_REPO_ROOT. Prompt impose un palier honnête. 75 tests, 0 vuln, compose OK. Palier de risque : privilégié (l'agent écrit+commit) — derrière flag + Phase 2 ; l'asset produit n'est jamais exécuté, il reste sous review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { AssetRepository } from "../src/gatekeeper/repository.js";
|
||||
import { Gatekeeper } from "../src/gatekeeper/gatekeeper.js";
|
||||
import { GitCommitter } from "../src/autoext/git-committer.js";
|
||||
import { AutoExtensionService } from "../src/autoext/auto-extension.js";
|
||||
import { buildProposeAssetTool } from "../src/autoext/tool.js";
|
||||
import { NullAlertSender } from "../src/alerts/sender.js";
|
||||
import { createLogger } from "../src/audit/log.js";
|
||||
import type { ToolSpec } from "../src/agent/types.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const log = createLogger("silent");
|
||||
|
||||
describe("gatekeeper autorise un outil sanctionné", () => {
|
||||
it("propose_asset (privilégié + sanctioned) est autorisé sans review", () => {
|
||||
const repo = new AssetRepository(":memory:");
|
||||
const gk = new Gatekeeper(repo, log);
|
||||
const spec: ToolSpec = {
|
||||
name: "chlova.propose_asset",
|
||||
description: "",
|
||||
parameters: {},
|
||||
server: "chlova",
|
||||
readOnly: false,
|
||||
riskTier: "privileged",
|
||||
sanctioned: true,
|
||||
};
|
||||
expect(gk.authorizeTool(spec).allowed).toBe(true);
|
||||
repo.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("propose_asset tool", () => {
|
||||
let root: string;
|
||||
let repo: AssetRepository;
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), "chlova-tool-"));
|
||||
await exec("git", ["init", "-q"], { cwd: root });
|
||||
await exec("git", ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "--allow-empty", "-q", "-m", "init"], { cwd: root });
|
||||
repo = new AssetRepository(":memory:");
|
||||
});
|
||||
afterEach(async () => {
|
||||
repo.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("exécute la proposition et renvoie un résumé", async () => {
|
||||
const svc = new AutoExtensionService(repo, new GitCommitter(root), new NullAlertSender(log), log, root);
|
||||
const tool = buildProposeAssetTool(svc);
|
||||
expect(tool.spec.sanctioned).toBe(true);
|
||||
const out = await tool.execute({
|
||||
type: "tool",
|
||||
name: "Ping Host",
|
||||
version: "1.0.0",
|
||||
riskTier: "reversible",
|
||||
summary: "ping",
|
||||
content: "{}",
|
||||
});
|
||||
expect(out).toContain("need-review");
|
||||
expect(repo.listByStatus("provisoire")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user