305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const test = require("node:test");
|
|
const versions = require("../../lib/aas-v1/versions");
|
|
const { canonicalJson, sha256 } = require("../../lib/aas-v1/canonical-json");
|
|
const { buildPlanEnvelope, validateManifest, validatePlanEnvelope } = require("../../lib/aas-v1/stack");
|
|
const { sanitizeValidationDetails, validateInstance } = require("../../lib/aas-v1/schema-validator");
|
|
|
|
const DIGEST_A = `sha256-${"a".repeat(64)}`;
|
|
const DIGEST_B = `sha256-${"b".repeat(64)}`;
|
|
const DIGEST_C = `sha256-${"c".repeat(64)}`;
|
|
const DIGEST_D = `sha256-${"d".repeat(64)}`;
|
|
const DIGEST_E = `sha256-${"e".repeat(64)}`;
|
|
const DIGEST_F = `sha256-${"f".repeat(64)}`;
|
|
|
|
function manifest(overrides = {}) {
|
|
return {
|
|
schemaVersion: 2,
|
|
name: "react-vite-production",
|
|
catalog: {
|
|
package: "agentic-awesome-skills",
|
|
version: "15.0.0",
|
|
integrity: DIGEST_A,
|
|
},
|
|
targets: [
|
|
{ host: "codex", scope: "project" },
|
|
{ host: "claude", scope: "user" },
|
|
],
|
|
profile: {
|
|
goals: ["build", "test", "deploy"],
|
|
projectType: "web application",
|
|
languages: ["typescript"],
|
|
frameworks: ["react", "vite"],
|
|
constraints: ["production-ready"],
|
|
},
|
|
skills: [
|
|
{ id: "react-best-practices" },
|
|
{ id: "testing/playwright-skill" },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function planInput(overrides = {}) {
|
|
const value = manifest();
|
|
const installedEntries = [
|
|
{ skillId: "react-best-practices", treeDigest: DIGEST_E, catalogIntegrity: DIGEST_A },
|
|
];
|
|
const nextEntries = [
|
|
{ skillId: "react-best-practices", treeDigest: DIGEST_B, catalogIntegrity: DIGEST_A },
|
|
{ skillId: "testing/playwright-skill", treeDigest: DIGEST_F, catalogIntegrity: DIGEST_A },
|
|
];
|
|
return {
|
|
manifest: value,
|
|
handshake: { ...versions },
|
|
catalog: { ...value.catalog },
|
|
runtime: {
|
|
package: "agentic-awesome-skills",
|
|
version: "15.0.0",
|
|
integrity: "sha512-VTOb3O9PSYKCDO99i3h0vOn7vHQlGtO/+jSErR80g6OGaDJoBzg3q2GE9Nu890en1/Z54hBEYiVQj/1Rl95xEg==",
|
|
closureDigest: DIGEST_B,
|
|
},
|
|
target: {
|
|
host: "codex",
|
|
scope: "project",
|
|
adapterVersion: "1.0.0",
|
|
identityDigest: DIGEST_C,
|
|
},
|
|
installedState: {
|
|
digest: sha256(canonicalJson({ schemaVersion: 1, entries: installedEntries })),
|
|
entries: installedEntries,
|
|
},
|
|
operations: [
|
|
{
|
|
kind: "install",
|
|
skillId: "testing/playwright-skill",
|
|
sourceTreeDigest: DIGEST_F,
|
|
expectedTreeDigest: null,
|
|
resultTreeDigest: DIGEST_F,
|
|
backupRequired: false,
|
|
},
|
|
{
|
|
kind: "replaceManaged",
|
|
skillId: "react-best-practices",
|
|
sourceTreeDigest: DIGEST_B,
|
|
expectedTreeDigest: DIGEST_E,
|
|
resultTreeDigest: DIGEST_B,
|
|
backupRequired: true,
|
|
},
|
|
],
|
|
overrides: [{
|
|
kind: "managedDrift",
|
|
skillId: "react-best-practices",
|
|
reasonCodes: ["AAS_PLAN_MANAGED_DRIFT_APPROVED", "AAS_PLAN_BACKUP_REQUIRED"],
|
|
}],
|
|
stateCommit: {
|
|
previousDigest: sha256(canonicalJson({ schemaVersion: 1, entries: installedEntries })),
|
|
nextDigest: sha256(canonicalJson({ schemaVersion: 1, entries: nextEntries })),
|
|
position: "final",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("validateManifest returns a canonical digest with the agent-provided profile", () => {
|
|
const value = manifest();
|
|
const result = validateManifest(value);
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.status, "valid");
|
|
assert.equal(result.manifestDigest, sha256(canonicalJson(value)));
|
|
assert.deepEqual(
|
|
{
|
|
protocolVersion: result.protocolVersion,
|
|
coreVersion: result.coreVersion,
|
|
catalogSchemaVersion: result.catalogSchemaVersion,
|
|
},
|
|
versions,
|
|
);
|
|
|
|
const withLegacyPolicy = { ...value, policy: { requireKnownSource: true } };
|
|
const invalid = validateManifest(withLegacyPolicy);
|
|
assert.equal(invalid.ok, false);
|
|
assert(invalid.details.issues.some((entry) => (
|
|
entry.field === "input"
|
|
&& entry.keyword === "additionalProperties"
|
|
&& entry.limit === false
|
|
&& entry.code === "AAS_STACK_FIELD_UNKNOWN"
|
|
)));
|
|
});
|
|
|
|
test("validateManifest reports bounded path-safe profile diagnostics without echoing input", () => {
|
|
const sensitiveKey = "/Users/example/private/TOKEN_CANARY";
|
|
const cases = [
|
|
{
|
|
value: manifest({ profile: { ...manifest().profile, goals: ["x".repeat(129)] } }),
|
|
expected: { field: "profile.goals[]", keyword: "maxLength", limit: 128 },
|
|
},
|
|
{
|
|
value: manifest({ profile: { ...manifest().profile, languages: Array.from({ length: 33 }, (_, index) => `lang-${index}`) } }),
|
|
expected: { field: "profile.languages", keyword: "maxItems", limit: 32 },
|
|
},
|
|
{
|
|
value: manifest({ profile: { ...manifest().profile, [sensitiveKey]: "SECRET_VALUE_CANARY" } }),
|
|
expected: { field: "profile", keyword: "additionalProperties", limit: false },
|
|
},
|
|
{
|
|
value: manifest({ profile: "SECRET_PROFILE_CANARY" }),
|
|
expected: { field: "profile", keyword: "type", limit: "object" },
|
|
},
|
|
];
|
|
|
|
for (const { value, expected } of cases) {
|
|
const result = validateManifest(value);
|
|
assert.equal(result.ok, false);
|
|
assert(result.details.issues.some((entry) => (
|
|
entry.field === expected.field
|
|
&& entry.keyword === expected.keyword
|
|
&& entry.limit === expected.limit
|
|
)), `missing diagnostic ${JSON.stringify(expected)}`);
|
|
const serialized = JSON.stringify(result);
|
|
assert.doesNotMatch(serialized, /Users|TOKEN_CANARY|SECRET_VALUE_CANARY|SECRET_PROFILE_CANARY|schemaPath|instancePath/);
|
|
assert(result.details.issues.length <= 32);
|
|
}
|
|
});
|
|
|
|
test("schema diagnostics redact dynamic map keys even when they look path-safe", () => {
|
|
const details = sanitizeValidationDetails({
|
|
issues: [{
|
|
instancePath: "/metadata/TOKEN_SECRET_CANARY",
|
|
keyword: "type",
|
|
params: { type: "string" },
|
|
}],
|
|
});
|
|
assert.deepEqual(details, {
|
|
issues: [{ field: "metadata.[unknown]", keyword: "type", limit: "string" }],
|
|
});
|
|
assert.doesNotMatch(JSON.stringify(details), /TOKEN_SECRET_CANARY/);
|
|
});
|
|
|
|
test("validateManifest rejects duplicates, unsafe ids, legacy policy, and unsupported targets", () => {
|
|
const value = manifest({
|
|
targets: [
|
|
{ host: "codex", scope: "project" },
|
|
{ host: "codex", scope: "project" },
|
|
{ host: "gemini", scope: "project" },
|
|
],
|
|
profile: {
|
|
goals: ["build"],
|
|
languages: ["typescript", "typescript"],
|
|
frameworks: [],
|
|
constraints: [],
|
|
},
|
|
policy: { requireKnownSource: true },
|
|
skills: [{ id: "../escape" }, { id: "same" }, { id: "same" }],
|
|
});
|
|
const result = validateManifest(value);
|
|
assert.equal(result.ok, false);
|
|
const codes = new Set(result.details.issues.map((entry) => entry.code));
|
|
for (const code of [
|
|
"AAS_STACK_TARGET_DUPLICATE",
|
|
"AAS_STACK_TARGET_HOST_UNSUPPORTED",
|
|
"AAS_STACK_FIELD_UNKNOWN",
|
|
"AAS_STACK_VALUE_DUPLICATE",
|
|
"AAS_STACK_STRING_FORMAT_INVALID",
|
|
"AAS_STACK_SKILL_DUPLICATE",
|
|
]) assert(codes.has(code), `missing issue code ${code}`);
|
|
});
|
|
|
|
test("buildPlanEnvelope is byte-deterministic, single-target, path-free, and digest-bound", () => {
|
|
const firstInput = planInput();
|
|
const secondInput = planInput({
|
|
operations: [...firstInput.operations].reverse(),
|
|
overrides: [{
|
|
...firstInput.overrides[0],
|
|
reasonCodes: [...firstInput.overrides[0].reasonCodes].reverse(),
|
|
}],
|
|
});
|
|
const first = buildPlanEnvelope(firstInput);
|
|
const second = buildPlanEnvelope(secondInput);
|
|
|
|
assert.equal(canonicalJson(first), canonicalJson(second));
|
|
assert.equal(first.kind, "aas.stack-plan");
|
|
assert.equal(first.digest, sha256(canonicalJson(first.payload)));
|
|
assert.deepEqual(first.payload.target, firstInput.target);
|
|
assert.equal(Object.hasOwn(first.payload.target, "path"), false);
|
|
assert.deepEqual(first.payload.desiredSkills, ["react-best-practices", "testing/playwright-skill"]);
|
|
assert.deepEqual(first.payload.operations.map((entry) => entry.kind), ["replaceManaged", "install"]);
|
|
assert.equal(first.payload.stateCommit.position, "final");
|
|
|
|
const tampered = structuredClone(first);
|
|
tampered.payload.operations[0].resultTreeDigest = DIGEST_C;
|
|
assert.notEqual(tampered.digest, sha256(canonicalJson(tampered.payload)));
|
|
});
|
|
|
|
test("buildPlanEnvelope fails closed on handshake, catalog, target, drift, and physical path input", () => {
|
|
assert.throws(
|
|
() => buildPlanEnvelope(planInput({ handshake: { ...versions, catalogSchemaVersion: "9.0.0" } })),
|
|
(error) => error.code === "AAS_PLAN_VERSION_INCOMPATIBLE" && error.category === "incompatibleVersion",
|
|
);
|
|
assert.throws(
|
|
() => buildPlanEnvelope(planInput({ catalog: { ...manifest().catalog, integrity: DIGEST_B } })),
|
|
(error) => error.code === "AAS_PLAN_CATALOG_MISMATCH",
|
|
);
|
|
assert.throws(
|
|
() => buildPlanEnvelope(planInput({ target: { host: "claude", scope: "project", adapterVersion: "1.0.0", identityDigest: DIGEST_C } })),
|
|
(error) => error.code === "AAS_PLAN_TARGET_NOT_IN_MANIFEST",
|
|
);
|
|
assert.throws(
|
|
() => buildPlanEnvelope(planInput({ stateCommit: { previousDigest: DIGEST_A, nextDigest: DIGEST_F, position: "final" } })),
|
|
(error) => error.code === "AAS_PLAN_STATE_COMMIT_MISMATCH",
|
|
);
|
|
assert.throws(
|
|
() => buildPlanEnvelope(planInput({ target: { ...planInput().target, path: "/tmp/escape" } })),
|
|
(error) => error.code === "AAS_PLAN_FIELD_UNKNOWN" && error.details.keys.includes("path"),
|
|
);
|
|
});
|
|
|
|
test("validatePlanEnvelope rejects a re-digested incompatible or internally inconsistent plan", () => {
|
|
const plan = buildPlanEnvelope(planInput());
|
|
assert.equal(validatePlanEnvelope(plan), plan);
|
|
|
|
const incompatible = structuredClone(plan);
|
|
incompatible.payload.versions.catalogSchemaVersion = "9.0.0";
|
|
incompatible.digest = sha256(canonicalJson(incompatible.payload));
|
|
assert.throws(() => validatePlanEnvelope(incompatible), { code: "AAS_PLAN_VERSION_INCOMPATIBLE" });
|
|
|
|
const inconsistent = structuredClone(plan);
|
|
inconsistent.payload.stateCommit.nextDigest = DIGEST_C;
|
|
inconsistent.digest = sha256(canonicalJson(inconsistent.payload));
|
|
assert.throws(() => validatePlanEnvelope(inconsistent), { code: "AAS_PLAN_NEXT_STATE_MISMATCH" });
|
|
});
|
|
|
|
test("all public v1 stack schemas are valid JSON Schema documents with unique ids", () => {
|
|
const schemaDirectory = path.resolve(__dirname, "..", "..", "..", "schemas", "aas-v1");
|
|
const expected = [
|
|
"catalog-manifest.schema.json",
|
|
"catalog-metadata.schema.json",
|
|
"doctor-result.schema.json",
|
|
"journal.schema.json",
|
|
"managed-state.schema.json",
|
|
"plan.schema.json",
|
|
"recovery-plan.schema.json",
|
|
"result-envelope.schema.json",
|
|
"selection-input.schema.json",
|
|
"stack-manifest.schema.json",
|
|
];
|
|
const ids = new Set();
|
|
for (const name of expected) {
|
|
const schema = JSON.parse(fs.readFileSync(path.join(schemaDirectory, name), "utf8"));
|
|
assert.equal(schema.$schema, "https://json-schema.org/draft/2020-12/schema");
|
|
assert.match(schema.$id, /^urn:agentic-awesome-skills:schema:/);
|
|
assert.equal(ids.has(schema.$id), false, `duplicate schema id ${schema.$id}`);
|
|
ids.add(schema.$id);
|
|
}
|
|
});
|
|
|
|
test("the generated offline catalog manifest validates against its public schema", () => {
|
|
const manifestPath = path.resolve(__dirname, "..", "..", "..", "data", "aas-v1", "catalog-manifest.v1.json");
|
|
const instance = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
assert.equal(validateInstance("catalog-manifest.schema.json", instance), instance);
|
|
});
|