@suluk/agents 0.1.0

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,41 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { agentManifest, verifyAgentFreshness, contentHash } from "../src/index";
3
+ import { coninDoc, coninInstructions } from "./fixtures/conin";
4
+
5
+ describe("C027 signable agent manifest", () => {
6
+ const m = agentManifest(coninDoc, "conin");
7
+
8
+ test("canonical: root + reachable sub-tree, sorted, no escalations", () => {
9
+ expect(m.manifestVersion).toBe(1);
10
+ expect(m.agent).toBe("conin");
11
+ expect(m.nodes.map((n) => n.name)).toEqual(["conin", "coninRetrieval"]);
12
+ expect(m.escalations).toEqual([]);
13
+ expect(m.reachable.tools).toEqual(["find_comparables", "generate_deliverable", "run_core_primitive", "search_library"]);
14
+ });
15
+
16
+ test("carries each skill's contentHash (so a signature over the manifest covers preprompt drift)", () => {
17
+ const conin = m.nodes.find((n) => n.name === "conin")!;
18
+ expect(conin.skills[0].contentHash).toBe("sha256-9f2c0000deadbeef");
19
+ expect(conin.effectiveScope).toEqual(["project:read", "deliverable:write", "library:read"]);
20
+ const retr = m.nodes.find((n) => n.name === "coninRetrieval")!;
21
+ expect(retr.effectiveScope).toEqual(["library:read"]);
22
+ });
23
+
24
+ test("deterministic — twice equal", () => {
25
+ expect(agentManifest(coninDoc, "conin")).toEqual(m);
26
+ });
27
+
28
+ test("freshness: a drifted served snapshot is caught against the signed contentHash", () => {
29
+ // declared hashes in the fixture are placeholders → any real snapshot drifts
30
+ const drift = verifyAgentFreshness(m, { "conin/operate": coninInstructions.operate });
31
+ expect(drift.map((f) => f.code)).toContain("stale-skill");
32
+ // a snapshot whose hash MATCHES the signed one is fresh
33
+ const matchHash = m.nodes.find((n) => n.name === "conin")!.skills[0].contentHash!;
34
+ const synthetic = "X".repeat(3);
35
+ // build a manifest whose skill hash equals the synthetic snapshot's hash, then verify fresh
36
+ const m2 = structuredClone(m);
37
+ m2.nodes.find((n) => n.name === "conin")!.skills[0].contentHash = contentHash(synthetic);
38
+ expect(verifyAgentFreshness(m2, { "conin/operate": synthetic }).filter((f) => f.code === "stale-skill")).toEqual([]);
39
+ expect(matchHash.startsWith("sha256-")).toBe(true);
40
+ });
41
+ });
@@ -0,0 +1,56 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import type { OpenAPIv4Document } from "@suluk/core";
3
+ import { skillModels, resolveSkillModels, SEED_CATALOG } from "../src/index";
4
+
5
+ /** An agent with routes (⇒ needs tool-calling) and two skills: a needs-based one + an explicit opt-out. */
6
+ function doc(): OpenAPIv4Document {
7
+ return {
8
+ openapi: "4.0.0-candidate",
9
+ info: { title: "x", version: "0" },
10
+ paths: { "v1/s": { requests: { search: { method: "get", responses: { ok: { status: 200 } } } } } },
11
+ "x-suluk-agents": {
12
+ conin: {
13
+ description: "orchestrator with a tool",
14
+ routes: { search: { operationRef: "#/paths/v1~1s/requests/search" } },
15
+ skills: {
16
+ operate: { modelProfile: "cheap-fast" }, // needs-based ⇒ catalog selects
17
+ legacy: { model: ["anthropic/claude-opus-4"] }, // explicit list ⇒ opt-out, returned verbatim
18
+ },
19
+ },
20
+ },
21
+ } as OpenAPIv4Document;
22
+ }
23
+
24
+ describe("C027 × @suluk/models — the model-selection seam", () => {
25
+ test("a needs-based skill resolves to a catalog SELECTION (tool-calling derived from the agent's routes)", () => {
26
+ const r = skillModels(doc(), "conin", "operate", SEED_CATALOG);
27
+ expect(r.from).toBe("selected");
28
+ expect(r.ids.length).toBeGreaterThan(0);
29
+ expect(r.snapshotHash).toBe(SEED_CATALOG.snapshotHash); // reproducible pin
30
+ // hasRoutes ⇒ needsTools was derived ⇒ the winner passed the tool-calling filter
31
+ expect(r.selection!.ranked[0].why.passedFilters).toContain("tool-calling");
32
+ // cheap-fast ⇒ a cheap model, never the premium ones
33
+ expect(["anthropic/claude-opus-4", "openai/gpt-5"]).not.toContain(r.ids[0]);
34
+ });
35
+
36
+ test("an explicit model[] with no profile is the author's OPT-OUT — returned verbatim", () => {
37
+ const r = skillModels(doc(), "conin", "legacy", SEED_CATALOG);
38
+ expect(r.from).toBe("declared");
39
+ expect(r.ids).toEqual(["anthropic/claude-opus-4"]);
40
+ expect(r.snapshotHash).toBeNull();
41
+ });
42
+
43
+ test("the analyzer's minWindowRequired flows in as a hard min-context filter", () => {
44
+ // require a 500k window ⇒ only the 1M-window seed models survive
45
+ const sel = resolveSkillModels(doc(), "conin", "operate", SEED_CATALOG, 500000);
46
+ const ids = sel.ranked.map((x) => x.id);
47
+ expect(ids.every((id) => ["google/gemini-2.5-flash", "google/gemini-2.5-pro", "meta-llama/llama-4-maverick"].includes(id))).toBe(true);
48
+ });
49
+
50
+ test("the C028 modelAllowlist is the TERMINAL MEET — selection is restricted to it", () => {
51
+ const d = doc();
52
+ d["x-suluk-policy"] = { fleet: { appliesTo: ["#/x-suluk-agents/conin"], modelAllowlist: ["google/gemini-2.5-flash"] } };
53
+ const r = skillModels(d, "conin", "operate", SEED_CATALOG);
54
+ expect(r.ids).toEqual(["google/gemini-2.5-flash"]); // even though cheap-fast might prefer gpt-4o-mini
55
+ });
56
+ });
@@ -0,0 +1,103 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import type { OpenAPIv4Document } from "@suluk/core";
3
+ import {
4
+ effectiveUnderPolicies, policyConstrain, lintPolicy, policyOk,
5
+ agentManifest, assertServedSubsetGoverned,
6
+ } from "../src/index";
7
+ import { coninDoc } from "./fixtures/conin";
8
+
9
+ /** Conin under an operator policy that narrows every axis. */
10
+ function governed(): OpenAPIv4Document {
11
+ const d = structuredClone(coninDoc);
12
+ d["x-suluk-policy"] = {
13
+ "acme-fleet": {
14
+ appliesTo: ["#/x-suluk-agents/conin", "#/x-suluk-agents/coninRetrieval"],
15
+ scopeAllowlist: ["project:read", "library:read"], // drops deliverable:write
16
+ tools: { deny: ["run_core_primitive"] },
17
+ capTier: "resident", // conin.operate is cold-tail → resident
18
+ modelAllowlist: ["google/gemini-2.5-flash"], // drops opus
19
+ maxDepthCap: 0,
20
+ forbidNesting: true, // removes the retrieval sub-agent
21
+ costCeiling: { amount: 5000, amountUnit: "micro-usd", basis: "per-request", enforcedBy: "adapter" },
22
+ },
23
+ };
24
+ return d;
25
+ }
26
+
27
+ describe("C028 policyConstrain — monotone-narrowing MEET (effective = INTERSECT(operator, agent))", () => {
28
+ const d = governed();
29
+ const { effective, narrowings } = effectiveUnderPolicies(d, "conin");
30
+
31
+ test("narrows scope, model, tier, tools, depth, nesting — never widens", () => {
32
+ expect(effective.scope).toEqual(["project:read", "library:read"]); // deliverable:write removed
33
+ const operate = effective.skills.find((s) => s.name === "operate")!;
34
+ expect(operate.model).toEqual(["google/gemini-2.5-flash"]); // opus dropped
35
+ expect(operate.tier).toBe("resident"); // capped from cold-tail
36
+ expect(operate.usable).toBe(true);
37
+ expect(effective.allowedTools).toEqual(["generate_deliverable"]);
38
+ expect(effective.deniedTools).toEqual(["run_core_primitive"]);
39
+ expect(effective.nestingForbidden).toBe(true);
40
+ expect(effective.maxDepth).toBe(0);
41
+ expect(effective.deniedSubAgents).toEqual(["retrieval"]);
42
+ expect(narrowings.map((n) => n.axis).sort()).toEqual(["model", "nesting", "scope", "tier", "tools"]);
43
+ });
44
+
45
+ test("the MEET never grants a capability the agent did not self-declare", () => {
46
+ // effective scope ⊆ agent scope
47
+ const declared = d["x-suluk-agents"]!.conin.scope!;
48
+ expect(effective.scope!.every((s) => declared.includes(s))).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe("C028 lintPolicy", () => {
53
+ test("a satisfiable, well-formed policy lints clean (no errors)", () => {
54
+ const findings = lintPolicy(governed());
55
+ expect(findings.filter((f) => f.severity === "error")).toEqual([]);
56
+ expect(policyOk(findings)).toBe(true);
57
+ });
58
+
59
+ test("NAMED failure: a modelAllowlist that excludes every model of a skill is unsatisfiable", () => {
60
+ const d = governed();
61
+ d["x-suluk-policy"]!["acme-fleet"].modelAllowlist = ["openai/gpt-9"]; // matches no Conin skill model
62
+ expect(lintPolicy(d).some((f) => f.code === "policy-unsatisfiable")).toBe(true);
63
+ });
64
+
65
+ test("appliesTo must resolve to a real agent (dangling caught)", () => {
66
+ const d = governed();
67
+ d["x-suluk-policy"]!["acme-fleet"].appliesTo = ["#/x-suluk-agents/ghost"];
68
+ expect(lintPolicy(d).some((f) => f.code === "policy-applies-dangling")).toBe(true);
69
+ });
70
+
71
+ test("D1: a request-value selector smuggled into a policy is forbidden", () => {
72
+ const d = governed();
73
+ d["x-suluk-policy"]!["acme-fleet"]["x-when"] = "{$request.body#/tier}";
74
+ expect(lintPolicy(d).some((f) => f.code === "request-value-selector")).toBe(true);
75
+ });
76
+
77
+ test("cap-below-estimate: an operator cap under the agent's own estimate is flagged (cross-facet, static)", () => {
78
+ const d = governed();
79
+ d["x-suluk-agents"]!.conin["x-suluk-cost"] = { estimateMicroUsd: 10000 };
80
+ // cap = 5000 µ$ < estimate 10000 µ$
81
+ expect(lintPolicy(d).some((f) => f.code === "cap-below-estimate")).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe("C028 manifest + conformance folds", () => {
86
+ test("the signed manifest carries the operator-effective surface (so the signature covers caps)", () => {
87
+ const m = agentManifest(governed(), "conin");
88
+ const conin = m.nodes.find((n) => n.name === "conin")!;
89
+ expect(conin.governed).toBeDefined();
90
+ expect(conin.governed!.allowedTools).toEqual(["generate_deliverable"]);
91
+ expect(conin.governed!.deniedTools).toEqual(["run_core_primitive"]);
92
+ expect(conin.governed!.nestingForbidden).toBe(true);
93
+ expect(conin.governed!.maxDepth).toBe(0);
94
+ // an ungoverned agent has no `governed` field
95
+ expect(agentManifest(coninDoc, "conin").nodes.find((n) => n.name === "conin")!.governed).toBeUndefined();
96
+ });
97
+
98
+ test("NAMED failure: serving a policy-DENIED tool is an over-serve (operator cap not holding on the wire)", () => {
99
+ const findings = assertServedSubsetGoverned(governed(), "conin", ["generate_deliverable", "run_core_primitive"]);
100
+ expect(findings.map((f) => f.code)).toEqual(["policy-denied-served"]);
101
+ expect(findings[0].detail).toContain("run_core_primitive");
102
+ });
103
+ });
@@ -0,0 +1,103 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { projectClaudePlugin, projectOpenRouter, contentHash, residentSurface, assertDefaultServedResident } from "../src/index";
3
+ import { coninDoc, coninInstructions, coninDayOne } from "./fixtures/conin";
4
+
5
+ describe("C027 Claude-plugin projection", () => {
6
+ const plug = projectClaudePlugin(coninDoc, "conin", {
7
+ mcpUrl: "https://construction-intelligence.saastemly.com/mcp",
8
+ version: "1.0.0",
9
+ homepage: "https://construction-intelligence.saastemly.com",
10
+ instructions: coninInstructions,
11
+ });
12
+
13
+ test("emits plugin.json + .mcp.json + a generated SKILL.md", () => {
14
+ expect(Object.keys(plug.files).sort()).toEqual([".mcp.json", "plugin.json", "skills/operate/SKILL.md"]);
15
+ const pj = JSON.parse(plug.files["plugin.json"]);
16
+ expect(pj.name).toBe("conin");
17
+ expect(pj.mcpServers).toBe("./.mcp.json");
18
+ expect(pj.description.length).toBeGreaterThan(10);
19
+ });
20
+
21
+ test(".mcp.json is HTTP MCP with host-side OAuth and NO embedded credential", () => {
22
+ const mj = JSON.parse(plug.files[".mcp.json"]);
23
+ expect(mj.mcpServers.conin.type).toBe("http");
24
+ expect(mj.mcpServers.conin.url).toContain("/mcp");
25
+ expect(mj.mcpServers.conin.oauth).toEqual({});
26
+ // no token / bearer / secret may ever appear in a projected artifact
27
+ expect(plug.files[".mcp.json"]).not.toMatch(/bearer|token|secret|api[_-]?key/i);
28
+ });
29
+
30
+ test("SKILL.md carries the contentHash + version staleness stamp (the genuinely-missing feature)", () => {
31
+ const md = plug.files["skills/operate/SKILL.md"];
32
+ expect(md).toContain("name: operate");
33
+ expect(md).toContain(`contentHash: ${contentHash(coninInstructions.operate)}`);
34
+ expect(md).toContain("version: 2026-06-11");
35
+ expect(md).toContain("source: https://construction-intelligence.saastemly.com/v1/instructions");
36
+ expect(md.trimEnd().endsWith(coninInstructions.operate)).toBe(true);
37
+ });
38
+
39
+ test("projection is a PURE FUNCTION — same contract in, byte-identical artifacts out", () => {
40
+ const again = projectClaudePlugin(coninDoc, "conin", {
41
+ mcpUrl: "https://construction-intelligence.saastemly.com/mcp",
42
+ version: "1.0.0",
43
+ homepage: "https://construction-intelligence.saastemly.com",
44
+ instructions: coninInstructions,
45
+ });
46
+ expect(again).toEqual(plug);
47
+ });
48
+ });
49
+
50
+ describe("C027 OpenRouter projection", () => {
51
+ const m = projectOpenRouter(coninDoc, "conin", { instructions: coninInstructions });
52
+
53
+ test("routes → function tools keyed by the wire id; model preference from the primary skill", () => {
54
+ expect(m.model).toEqual(["anthropic/claude-opus-4", "google/gemini-2.5-flash"]);
55
+ expect(m.tier).toBe("cold-tail");
56
+ expect(m.tools.map((t) => t.function.name).sort()).toEqual(["generate_deliverable", "run_core_primitive"]);
57
+ expect(m.tools.every((t) => t.type === "function")).toBe(true);
58
+ });
59
+
60
+ test("instructions is a pointer + a pinned contentHash, never the raw text by default", () => {
61
+ expect(m.instructions.source).toContain("/v1/instructions");
62
+ expect(m.instructions.contentHash).toBe(contentHash(coninInstructions.operate));
63
+ expect(m.instructions.version).toBe("2026-06-11");
64
+ });
65
+
66
+ test("sub-agents surface as front-door dispatch targets, by name", () => {
67
+ expect(m.subAgents).toEqual([{ name: "retrieval", ref: "#/x-suluk-agents/coninRetrieval" }]);
68
+ });
69
+
70
+ test("deterministic — twice equal", () => {
71
+ expect(projectOpenRouter(coninDoc, "conin", { instructions: coninInstructions })).toEqual(m);
72
+ });
73
+ });
74
+
75
+ describe("C027 tier-trim — cold-tail routes leave the default served surface (the real context reduction)", () => {
76
+ const tiered = structuredClone(coninDoc);
77
+ tiered["x-suluk-agents"]!.conin.routes!.run_core_primitive.tier = "cold-tail";
78
+ const m = projectOpenRouter(tiered, "conin", { instructions: coninInstructions });
79
+
80
+ test("default tools[] = resident + discover_tools; cold-tail moves to discoverable[]", () => {
81
+ expect(m.tools.map((t) => t.function.name).sort()).toEqual(["discover_tools", "generate_deliverable"]);
82
+ expect(m.discoverable.map((t) => t.function.name)).toEqual(["run_core_primitive"]);
83
+ });
84
+
85
+ test("no discover_tools meta when there is nothing to discover (all resident)", () => {
86
+ const allResident = projectOpenRouter(coninDoc, "conin", { instructions: coninInstructions });
87
+ expect(allResident.tools.some((t) => t.function.name === "discover_tools")).toBe(false);
88
+ expect(allResident.discoverable).toEqual([]);
89
+ });
90
+
91
+ test("residentSurface + the cold-tail-in-default conformance auditor", () => {
92
+ expect(residentSurface(tiered, "conin")).toEqual(["generate_deliverable"]);
93
+ expect(assertDefaultServedResident(tiered, "conin", ["generate_deliverable"])).toEqual([]);
94
+ expect(assertDefaultServedResident(tiered, "conin", ["generate_deliverable", "run_core_primitive"]).map((f) => f.code)).toEqual(["cold-tail-in-default"]);
95
+ });
96
+ });
97
+
98
+ describe("projection refuses a non-installable agent (fail-loud, not a broken artifact)", () => {
99
+ test("Conin's day-one dangling operationRef throws on BOTH targets", () => {
100
+ expect(() => projectOpenRouter(coninDayOne(), "conin")).toThrow(/does not install|run_core_primitive|dangling/);
101
+ expect(() => projectClaudePlugin(coninDayOne(), "conin", { mcpUrl: "https://x/mcp" })).toThrow(/does not install|run_core_primitive|dangling/);
102
+ });
103
+ });
@@ -0,0 +1,27 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { intersectScope, analyzeScopes, lintAgents } from "../src/index";
3
+ import { coninDoc, escalationDoc } from "./fixtures/conin";
4
+
5
+ describe("C027 scope intersection (least-privilege by construction)", () => {
6
+ test("intersectScope treats null as unconstrained", () => {
7
+ expect(intersectScope(null, ["a", "b"])).toEqual(["a", "b"]);
8
+ expect(intersectScope(["a", "b"], null)).toEqual(["a", "b"]);
9
+ expect(intersectScope(["a", "b", "c"], ["b", "c", "d"])).toEqual(["b", "c"]);
10
+ expect(intersectScope(null, null)).toBeNull();
11
+ });
12
+
13
+ test("a child's effective scope is the intersection with its caller's", () => {
14
+ const { effective, escalations } = analyzeScopes(coninDoc, "conin");
15
+ expect(escalations).toEqual([]);
16
+ expect(effective["conin"]).toEqual(["project:read", "deliverable:write", "library:read"]);
17
+ // retrieval declares only library:read; caller grants it → effective = library:read
18
+ expect(effective["coninRetrieval"]).toEqual(["library:read"]);
19
+ });
20
+
21
+ test("NAMED failure: a child needing a permission the caller doesn't grant is an escalation (confused-deputy)", () => {
22
+ const { escalations } = analyzeScopes(escalationDoc(), "conin");
23
+ expect(escalations).toEqual([{ parent: "conin", childLocal: "retrieval", child: "coninRetrieval", perms: ["library:read"] }]);
24
+ // and it blocks installation (an error-severity lint)
25
+ expect(lintAgents(escalationDoc()).some((f) => f.severity === "error" && f.code === "scope-escalation")).toBe(true);
26
+ });
27
+ });
@@ -0,0 +1,45 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { signRegistry, verifyRegistrySignature, generateSigningKeypair } from "@suluk/builder";
3
+ import { agentManifest, verifyAgentFreshness, contentHash } from "../src/index";
4
+ import { coninDoc, coninInstructions } from "./fixtures/conin";
5
+
6
+ /**
7
+ * The C027 marketplace supply-chain loop (council open-Q #8): an agent manifest is signed through the SAME
8
+ * @suluk/builder ECDSA-P256 registry signing (C021). Because the manifest carries each skill's contentHash, the
9
+ * signature COVERS the served preprompt — a preprompt that drifts after mint is detectable (verifyAgentFreshness),
10
+ * and any structural tamper breaks the signature. One mechanism, reused unchanged.
11
+ */
12
+ describe("C027 × C021 — a signed agent manifest covers preprompt drift", () => {
13
+ const snapshot = coninInstructions.operate;
14
+
15
+ test("sign → verify → preprompt-drift caught → structural-tamper caught", async () => {
16
+ // pin the operate skill's contentHash to the actual served snapshot, then mint a signature over the manifest
17
+ const manifest = agentManifest(coninDoc, "conin");
18
+ manifest.nodes.find((n) => n.name === "conin")!.skills[0].contentHash = contentHash(snapshot);
19
+
20
+ const { publicKey, privateKey } = await generateSigningKeypair();
21
+ const sig = await signRegistry(manifest, privateKey);
22
+
23
+ // (1) a faithfully-distributed manifest verifies, and its skills are fresh against the current snapshot
24
+ expect(await verifyRegistrySignature(manifest, sig, publicKey)).toBe(true);
25
+ expect(verifyAgentFreshness(manifest, { "conin/operate": snapshot }).filter((f) => f.code === "stale-skill")).toEqual([]);
26
+
27
+ // (2) PREPROMPT DRIFT after mint: the served text changes but the signed manifest does not — the signature still
28
+ // verifies (nothing in the artifact changed), yet the freshness check catches the drift via the signed hash
29
+ const drifted = snapshot + " ← edited on the server after signing";
30
+ expect(await verifyRegistrySignature(manifest, sig, publicKey)).toBe(true);
31
+ expect(verifyAgentFreshness(manifest, { "conin/operate": drifted }).map((f) => f.code)).toContain("stale-skill");
32
+
33
+ // (3) STRUCTURAL TAMPER: mutate the manifest (redirect a route) — the signature no longer verifies
34
+ const tampered = structuredClone(manifest);
35
+ tampered.nodes.find((n) => n.name === "conin")!.routes[0].operationRef = "#/paths/evil/requests/x";
36
+ expect(await verifyRegistrySignature(tampered, sig, publicKey)).toBe(false);
37
+
38
+ // (4) key-order independence: same content, different top-level insertion order, still verifies (canonicalJson)
39
+ const reordered = {
40
+ escalations: manifest.escalations, reachable: manifest.reachable,
41
+ nodes: manifest.nodes, agent: manifest.agent, manifestVersion: manifest.manifestVersion,
42
+ };
43
+ expect(await verifyRegistrySignature(reordered, sig, publicKey)).toBe(true);
44
+ });
45
+ });
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ { "extends": "../../tsconfig.base.json", "compilerOptions": { "types": ["bun"] }, "include": ["src", "test"] }