@multileaf/ai-workflow 0.1.0 → 0.1.2
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.
- package/CHANGELOG.md +29 -1
- package/README.md +316 -13
- package/dist/adapter.d.ts +1 -0
- package/dist/adapter.js +37 -1
- package/dist/bundled-resources.d.ts +9 -0
- package/dist/bundled-resources.js +29 -0
- package/dist/constants.js +1 -0
- package/dist/files.d.ts +10 -0
- package/dist/files.js +216 -7
- package/dist/generator.js +13 -3
- package/dist/interpreter.js +47 -6
- package/dist/lockfile.d.ts +1 -1
- package/dist/lockfile.js +6 -2
- package/dist/migration.d.ts +21 -2
- package/dist/migration.js +162 -21
- package/dist/ownership.d.ts +14 -0
- package/dist/ownership.js +83 -0
- package/dist/package-audit.d.ts +1 -0
- package/dist/package-audit.js +7 -3
- package/dist/package-contract.d.ts +1 -0
- package/dist/package-contract.js +136 -74
- package/dist/package-integrity.d.ts +5 -1
- package/dist/package-integrity.js +56 -1
- package/dist/providers.d.ts +2 -0
- package/dist/providers.js +27 -5
- package/dist/recommendations.d.ts +7 -0
- package/dist/recommendations.js +141 -10
- package/dist/scanner.js +28 -26
- package/dist/types.d.ts +2 -0
- package/dist/workflow.js +729 -147
- package/docs/adapter-model.md +3 -1
- package/docs/backlog.md +2 -2
- package/docs/package-model.md +5 -3
- package/docs/product-requirements.md +2 -0
- package/docs/project-intelligence.md +3 -1
- package/docs/release-backlog.md +1 -1
- package/docs/security.md +4 -0
- package/package.json +9 -5
- package/resources/package.yaml +37 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parseDocument, stringify } from "yaml";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { checksumPackage } from "./package-integrity.js";
|
|
4
|
+
import { RESOURCE_TYPES } from "./package-contract.js";
|
|
5
|
+
import { TARGETS } from "./types.js";
|
|
6
|
+
export function createOwnedFile(path, content, metadata = {}) {
|
|
7
|
+
validateOwnedPath(path);
|
|
8
|
+
return { path, checksum: `sha256-${checksumPackage(content)}`, ...metadata };
|
|
9
|
+
}
|
|
10
|
+
export function serializeOwnership(files) {
|
|
11
|
+
const ordered = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
12
|
+
const unique = new Set();
|
|
13
|
+
for (const file of ordered) {
|
|
14
|
+
validateOwnedPath(file.path);
|
|
15
|
+
if (unique.has(file.path))
|
|
16
|
+
throw new Error(`Ownership inventory contains a duplicate path: ${file.path}`);
|
|
17
|
+
unique.add(file.path);
|
|
18
|
+
if (!/^sha256-[a-f0-9]{64}$/.test(file.checksum))
|
|
19
|
+
throw new Error(`Ownership inventory checksum is invalid: ${file.path}`);
|
|
20
|
+
}
|
|
21
|
+
return stringify({ schema: 1, files: ordered }, { lineWidth: 0 });
|
|
22
|
+
}
|
|
23
|
+
export function parseOwnership(content) {
|
|
24
|
+
const document = parseDocument(content, { uniqueKeys: true, strict: true });
|
|
25
|
+
if (document.errors.length)
|
|
26
|
+
throw new Error(`Ownership inventory YAML is invalid: ${document.errors[0].message}`);
|
|
27
|
+
const value = document.toJS();
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
29
|
+
throw new Error("Ownership inventory must be a mapping.");
|
|
30
|
+
const inventory = value;
|
|
31
|
+
if (Object.keys(inventory).some((key) => !["schema", "files"].includes(key)))
|
|
32
|
+
throw new Error("Ownership inventory contains an unsupported field.");
|
|
33
|
+
if (inventory.schema !== 1 || !Array.isArray(inventory.files))
|
|
34
|
+
throw new Error("Ownership inventory schema or files section is invalid.");
|
|
35
|
+
const files = inventory.files.map((entry, index) => {
|
|
36
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
37
|
+
throw new Error(`Ownership inventory entry ${index} is invalid.`);
|
|
38
|
+
const file = entry;
|
|
39
|
+
if (Object.keys(file).some((key) => !["path", "checksum", "kind", "target", "resourceType", "resourceId"].includes(key)))
|
|
40
|
+
throw new Error(`Ownership inventory entry ${index} contains an unsupported field.`);
|
|
41
|
+
if (typeof file.path !== "string" || typeof file.checksum !== "string")
|
|
42
|
+
throw new Error(`Ownership inventory entry ${index} is missing its path or checksum.`);
|
|
43
|
+
validateOwnedPath(file.path);
|
|
44
|
+
if (!/^sha256-[a-f0-9]{64}$/.test(file.checksum))
|
|
45
|
+
throw new Error(`Ownership inventory checksum is invalid: ${file.path}`);
|
|
46
|
+
if (file.kind !== undefined &&
|
|
47
|
+
!["neutral-resource", "target-resource", "ai-init", "supporting-file"].includes(String(file.kind)))
|
|
48
|
+
throw new Error(`Ownership inventory kind is invalid: ${file.path}`);
|
|
49
|
+
if (file.target !== undefined && !TARGETS.includes(file.target))
|
|
50
|
+
throw new Error(`Ownership inventory target is invalid: ${file.path}`);
|
|
51
|
+
if (file.resourceType !== undefined &&
|
|
52
|
+
!RESOURCE_TYPES.includes(String(file.resourceType)))
|
|
53
|
+
throw new Error(`Ownership inventory resource type is invalid: ${file.path}`);
|
|
54
|
+
if (file.resourceId !== undefined &&
|
|
55
|
+
(typeof file.resourceId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(file.resourceId)))
|
|
56
|
+
throw new Error(`Ownership inventory resource id is invalid: ${file.path}`);
|
|
57
|
+
return {
|
|
58
|
+
path: file.path,
|
|
59
|
+
checksum: file.checksum,
|
|
60
|
+
...(file.kind !== undefined ? { kind: file.kind } : {}),
|
|
61
|
+
...(file.target !== undefined ? { target: file.target } : {}),
|
|
62
|
+
...(file.resourceType !== undefined
|
|
63
|
+
? { resourceType: file.resourceType }
|
|
64
|
+
: {}),
|
|
65
|
+
...(file.resourceId !== undefined ? { resourceId: file.resourceId } : {}),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
if (new Set(files.map(({ path }) => path)).size !== files.length)
|
|
69
|
+
throw new Error("Ownership inventory paths must be unique.");
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
function validateOwnedPath(path) {
|
|
73
|
+
if (!path ||
|
|
74
|
+
isAbsolute(path) ||
|
|
75
|
+
path.includes("\\") ||
|
|
76
|
+
path.includes(":") ||
|
|
77
|
+
[...path].some((character) => {
|
|
78
|
+
const code = character.codePointAt(0) ?? 0;
|
|
79
|
+
return code <= 0x1f || code === 0x7f;
|
|
80
|
+
}) ||
|
|
81
|
+
path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
82
|
+
throw new Error("Ownership inventory paths must be normalized project-relative paths.");
|
|
83
|
+
}
|
package/dist/package-audit.d.ts
CHANGED
|
@@ -17,3 +17,4 @@ export declare function assertKnownPermissions(permissions: string[]): void;
|
|
|
17
17
|
export declare function reviewPackagePermissions(pkg: PackageContract, approved: string[]): PermissionReview;
|
|
18
18
|
export declare function serializePermissionReview(pkg: PackageContract): string;
|
|
19
19
|
export declare function assertPackagePermissions(pkg: PackageContract, approved: string[]): void;
|
|
20
|
+
export declare function assertPermissions(permissions: string[], approved: string[]): void;
|
package/dist/package-audit.js
CHANGED
|
@@ -22,7 +22,11 @@ export function serializePermissionReview(pkg) {
|
|
|
22
22
|
return `Package permission review: ${pkg.id}@${pkg.version}\n${permissions.length ? permissions.map((permission) => `- ${permission} (${PACKAGE_PERMISSIONS[permission]})`).join("\n") : "- none"}\nDecision: ${permissions.length ? "explicit approval required" : "no permissions requested"}\n`;
|
|
23
23
|
}
|
|
24
24
|
export function assertPackagePermissions(pkg, approved) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
assertPermissions(pkg.permissions, approved);
|
|
26
|
+
}
|
|
27
|
+
export function assertPermissions(permissions, approved) {
|
|
28
|
+
assertKnownPermissions(permissions);
|
|
29
|
+
const unapproved = permissions.filter((permission) => !approved.includes(permission));
|
|
30
|
+
if (unapproved.length)
|
|
31
|
+
throw new Error(`Package permissions require approval: ${unapproved.join(", ")}`);
|
|
28
32
|
}
|
|
@@ -28,3 +28,4 @@ export type PackageContract = {
|
|
|
28
28
|
targets?: string[];
|
|
29
29
|
};
|
|
30
30
|
export declare function validatePackageContract(content: string, resourceExists?: (path: string) => boolean): PackageContract;
|
|
31
|
+
export declare function validateResourcePath(path: string, field?: string): void;
|
package/dist/package-contract.js
CHANGED
|
@@ -1,93 +1,155 @@
|
|
|
1
|
+
import { parseDocument } from "yaml";
|
|
1
2
|
export const RESOURCE_TYPES = ["skills", "rules", "agents", "hooks", "templates"];
|
|
3
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
4
|
+
"schema",
|
|
5
|
+
"id",
|
|
6
|
+
"version",
|
|
7
|
+
"description",
|
|
8
|
+
"language",
|
|
9
|
+
"provider",
|
|
10
|
+
"source",
|
|
11
|
+
"engines",
|
|
12
|
+
"dependencies",
|
|
13
|
+
"permissions",
|
|
14
|
+
"policies",
|
|
15
|
+
"provenance",
|
|
16
|
+
"targets",
|
|
17
|
+
"resources",
|
|
18
|
+
"provides",
|
|
19
|
+
]);
|
|
2
20
|
export function validatePackageContract(content, resourceExists = () => true) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"permissions",
|
|
12
|
-
"provenance",
|
|
13
|
-
];
|
|
14
|
-
for (const field of required) {
|
|
15
|
-
if (!new RegExp(`^${field}:`, "m").test(content))
|
|
16
|
-
throw new Error(`Package field '${field}' is required.`);
|
|
21
|
+
const document = parseDocument(content, { uniqueKeys: true, strict: true });
|
|
22
|
+
if (document.errors.length)
|
|
23
|
+
throw new Error(`Package manifest YAML is invalid: ${document.errors[0].message}`);
|
|
24
|
+
const value = document.toJS();
|
|
25
|
+
const root = record(value, "manifest");
|
|
26
|
+
for (const key of Object.keys(root)) {
|
|
27
|
+
if (!TOP_LEVEL_KEYS.has(key))
|
|
28
|
+
throw new Error(`Package field '${key}' is not supported.`);
|
|
17
29
|
}
|
|
18
|
-
const schema =
|
|
19
|
-
if (schema !==
|
|
20
|
-
throw new Error("Package schema must be version 1.");
|
|
21
|
-
const
|
|
30
|
+
const schema = root.schema;
|
|
31
|
+
if (schema !== 1)
|
|
32
|
+
throw new Error("Package field 'schema' must be version 1.");
|
|
33
|
+
const id = requiredString(root, "id");
|
|
34
|
+
const version = requiredString(root, "version");
|
|
35
|
+
const provider = requiredString(root, "provider");
|
|
36
|
+
const source = requiredString(root, "source");
|
|
37
|
+
optionalString(root, "description", "manifest");
|
|
38
|
+
optionalString(root, "language", "manifest");
|
|
39
|
+
const rawResources = record(root.resources, "resources");
|
|
40
|
+
for (const key of Object.keys(rawResources)) {
|
|
41
|
+
if (!RESOURCE_TYPES.includes(key))
|
|
42
|
+
throw new Error(`Package resource section '${key}' is not supported.`);
|
|
43
|
+
}
|
|
44
|
+
const resources = {};
|
|
22
45
|
for (const type of RESOURCE_TYPES) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
if (!(type in rawResources))
|
|
47
|
+
throw new Error(`Package resource section '${type}' is required (use [] when empty).`);
|
|
48
|
+
const entries = array(rawResources[type], `resources.${type}`);
|
|
49
|
+
resources[type] = entries.map((entry, index) => {
|
|
50
|
+
const item = record(entry, `resources.${type}[${index}]`);
|
|
51
|
+
assertKeys(item, new Set(["id", "version", "path"]), `resources.${type}[${index}]`);
|
|
52
|
+
const resource = {
|
|
53
|
+
id: requiredString(item, "id", `resources.${type}[${index}]`),
|
|
54
|
+
version: requiredString(item, "version", `resources.${type}[${index}]`),
|
|
55
|
+
path: requiredString(item, "path", `resources.${type}[${index}]`),
|
|
56
|
+
};
|
|
57
|
+
validateResourcePath(resource.path, `resources.${type}[${index}].path`);
|
|
28
58
|
if (!resourceExists(resource.path))
|
|
29
59
|
throw new Error(`Package resource path does not exist: ${resource.path}`);
|
|
30
|
-
|
|
60
|
+
return resource;
|
|
61
|
+
});
|
|
62
|
+
if (new Set(resources[type].map(({ id: resourceId }) => resourceId)).size !==
|
|
63
|
+
resources[type].length)
|
|
64
|
+
throw new Error(`Package resource identifiers in '${type}' must be unique.`);
|
|
31
65
|
}
|
|
32
|
-
if (!resources.
|
|
33
|
-
!resources.rules.length &&
|
|
34
|
-
!resources.agents.length &&
|
|
35
|
-
!resources.hooks.length &&
|
|
36
|
-
!resources.templates.length)
|
|
66
|
+
if (!RESOURCE_TYPES.some((type) => resources[type].length))
|
|
37
67
|
throw new Error("Package resources must contain at least one resource.");
|
|
68
|
+
const provenance = record(root.provenance, "provenance");
|
|
69
|
+
assertKeys(provenance, new Set(["source", "checksum"]), "provenance");
|
|
70
|
+
const provenanceSource = requiredString(provenance, "source", "provenance");
|
|
71
|
+
const checksum = optionalString(provenance, "checksum", "provenance");
|
|
72
|
+
const policies = root.policies === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: array(root.policies, "policies").map((entry, index) => {
|
|
75
|
+
const policy = record(entry, `policies[${index}]`);
|
|
76
|
+
assertKeys(policy, new Set(["id", "value"]), `policies[${index}]`);
|
|
77
|
+
return {
|
|
78
|
+
id: requiredString(policy, "id", `policies[${index}]`),
|
|
79
|
+
value: requiredString(policy, "value", `policies[${index}]`),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const engines = root.engines === undefined ? undefined : stringRecord(root.engines, "engines");
|
|
83
|
+
const provides = root.provides === undefined ? undefined : parseProvides(root.provides);
|
|
84
|
+
const targets = root.targets === undefined ? undefined : stringArray(root.targets, "targets");
|
|
38
85
|
return {
|
|
39
86
|
schema: 1,
|
|
40
|
-
id
|
|
41
|
-
version
|
|
42
|
-
provider
|
|
43
|
-
source
|
|
87
|
+
id,
|
|
88
|
+
version,
|
|
89
|
+
provider,
|
|
90
|
+
source,
|
|
44
91
|
resources,
|
|
45
|
-
dependencies:
|
|
46
|
-
permissions:
|
|
47
|
-
provenance: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
...(nestedScalar(content, "ai_workflow")
|
|
53
|
-
? { engines: { ai_workflow: nestedScalar(content, "ai_workflow") } }
|
|
54
|
-
: {}),
|
|
55
|
-
...(list(content, "targets").length ? { targets: list(content, "targets") } : {}),
|
|
92
|
+
dependencies: stringArray(root.dependencies, "dependencies"),
|
|
93
|
+
permissions: stringArray(root.permissions, "permissions"),
|
|
94
|
+
provenance: { source: provenanceSource, ...(checksum ? { checksum } : {}) },
|
|
95
|
+
...(policies ? { policies } : {}),
|
|
96
|
+
...(engines ? { engines } : {}),
|
|
97
|
+
...(provides ? { provides } : {}),
|
|
98
|
+
...(targets ? { targets } : {}),
|
|
56
99
|
};
|
|
57
100
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
101
|
+
export function validateResourcePath(path, field = "resource path") {
|
|
102
|
+
if (path.startsWith("/") ||
|
|
103
|
+
path.includes("\\") ||
|
|
104
|
+
path.includes(":") ||
|
|
105
|
+
path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
106
|
+
throw new Error(`Package field '${field}' must be a normalized relative path.`);
|
|
60
107
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
108
|
+
function record(value, field) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
110
|
+
throw new Error(`Package field '${field}' must be a mapping.`);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function array(value, field) {
|
|
114
|
+
if (!Array.isArray(value))
|
|
115
|
+
throw new Error(`Package field '${field}' must be a list (use [] when empty).`);
|
|
116
|
+
return value;
|
|
67
117
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
118
|
+
function requiredString(value, key, parent = "manifest") {
|
|
119
|
+
const result = value[key];
|
|
120
|
+
if (typeof result !== "string" || !result.trim())
|
|
121
|
+
throw new Error(`Package field '${parent === "manifest" ? key : `${parent}.${key}`}' must be a non-empty string.`);
|
|
122
|
+
return result.trim();
|
|
73
123
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
function optionalString(value, key, parent) {
|
|
125
|
+
if (value[key] === undefined)
|
|
126
|
+
return undefined;
|
|
127
|
+
return requiredString(value, key, parent);
|
|
128
|
+
}
|
|
129
|
+
function stringArray(value, field) {
|
|
130
|
+
return array(value, field).map((entry, index) => {
|
|
131
|
+
if (typeof entry !== "string" || !entry.trim())
|
|
132
|
+
throw new Error(`Package field '${field}[${index}]' must be a non-empty string.`);
|
|
133
|
+
return entry.trim();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function stringRecord(value, field) {
|
|
137
|
+
const result = record(value, field);
|
|
138
|
+
return Object.fromEntries(Object.entries(result).map(([key, entry]) => {
|
|
139
|
+
if (typeof entry !== "string" || !entry.trim())
|
|
140
|
+
throw new Error(`Package field '${field}.${key}' must be a non-empty string.`);
|
|
141
|
+
return [key, entry.trim()];
|
|
142
|
+
}));
|
|
79
143
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.split(",")
|
|
87
|
-
.map((item) => item.trim())
|
|
88
|
-
.filter(Boolean);
|
|
144
|
+
function parseProvides(value) {
|
|
145
|
+
const result = record(value, "provides");
|
|
146
|
+
for (const key of Object.keys(result))
|
|
147
|
+
if (!RESOURCE_TYPES.includes(key))
|
|
148
|
+
throw new Error(`Package provides section '${key}' is not supported.`);
|
|
149
|
+
return Object.fromEntries(Object.entries(result).map(([key, entries]) => [key, stringArray(entries, `provides.${key}`)]));
|
|
89
150
|
}
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
|
|
151
|
+
function assertKeys(value, allowed, field) {
|
|
152
|
+
for (const key of Object.keys(value))
|
|
153
|
+
if (!allowed.has(key))
|
|
154
|
+
throw new Error(`Package field '${field}.${key}' is not supported.`);
|
|
93
155
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { type PackageContract } from "./package-contract.js";
|
|
2
|
+
import type { FileSystem } from "./types.js";
|
|
1
3
|
export type PackageProvenance = {
|
|
2
4
|
checksum: string;
|
|
3
5
|
signature?: string;
|
|
4
6
|
signer?: string;
|
|
5
7
|
};
|
|
6
8
|
export type SignatureVerifier = (content: string, signature: string, signer?: string) => boolean;
|
|
7
|
-
export declare function checksumPackage(content: string): string;
|
|
9
|
+
export declare function checksumPackage(content: string | Uint8Array): string;
|
|
10
|
+
/** Hash the exact package manifest and all declared resource bytes using a stable, framed format. */
|
|
11
|
+
export declare function checksumPackageSnapshot(manifest: Uint8Array, pkg: PackageContract, root: string, fs: FileSystem, manifestPath?: string): string;
|
|
8
12
|
export declare function verifyPackageProvenance(content: string, provenance: PackageProvenance, verifySignature?: SignatureVerifier): void;
|
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateResourcePath } from "./package-contract.js";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
4
|
export function checksumPackage(content) {
|
|
3
|
-
|
|
5
|
+
const hash = createHash("sha256");
|
|
6
|
+
if (typeof content === "string")
|
|
7
|
+
hash.update(content, "utf8");
|
|
8
|
+
else
|
|
9
|
+
hash.update(content);
|
|
10
|
+
return hash.digest("hex");
|
|
11
|
+
}
|
|
12
|
+
/** Hash the exact package manifest and all declared resource bytes using a stable, framed format. */
|
|
13
|
+
export function checksumPackageSnapshot(manifest, pkg, root, fs, manifestPath = "package.yaml") {
|
|
14
|
+
validateResourcePath(manifestPath, "manifest path");
|
|
15
|
+
const files = [
|
|
16
|
+
{ path: manifestPath, bytes: manifest },
|
|
17
|
+
];
|
|
18
|
+
const paths = new Set([manifestPath]);
|
|
19
|
+
for (const resources of Object.values(pkg.resources)) {
|
|
20
|
+
for (const resource of resources) {
|
|
21
|
+
validateResourcePath(resource.path);
|
|
22
|
+
if (paths.has(resource.path))
|
|
23
|
+
throw new Error(`Package resource path is duplicated: ${resource.path}`);
|
|
24
|
+
paths.add(resource.path);
|
|
25
|
+
const absolutePath = resolve(root, resource.path);
|
|
26
|
+
const rel = relative(resolve(root), absolutePath);
|
|
27
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
28
|
+
throw new Error(`Package resource path escapes its package: ${resource.path}`);
|
|
29
|
+
if (!fs.exists(absolutePath))
|
|
30
|
+
throw new Error(`Package resource path does not exist: ${resource.path}`);
|
|
31
|
+
if (fs.pathType && fs.pathType(absolutePath) !== "file")
|
|
32
|
+
throw new Error(`Package resource must be a regular file: ${resource.path}`);
|
|
33
|
+
if (fs.realpath) {
|
|
34
|
+
const realRoot = fs.realpath(root);
|
|
35
|
+
const realPath = fs.realpath(absolutePath);
|
|
36
|
+
const realRelative = relative(realRoot, realPath);
|
|
37
|
+
if (realRelative === ".." ||
|
|
38
|
+
realRelative.startsWith(`..${sep}`) ||
|
|
39
|
+
isAbsolute(realRelative))
|
|
40
|
+
throw new Error(`Package resource path escapes its package: ${resource.path}`);
|
|
41
|
+
}
|
|
42
|
+
const bytes = fs.readBytes
|
|
43
|
+
? fs.readBytes(absolutePath)
|
|
44
|
+
: Buffer.from(fs.read(absolutePath), "utf8");
|
|
45
|
+
files.push({ path: resource.path, bytes });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
49
|
+
const hash = createHash("sha256").update("aiw-package-snapshot-v1\0", "utf8");
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const name = Buffer.from(file.path, "utf8");
|
|
52
|
+
const nameLength = Buffer.alloc(4);
|
|
53
|
+
nameLength.writeUInt32BE(name.length);
|
|
54
|
+
const size = Buffer.alloc(8);
|
|
55
|
+
size.writeBigUInt64BE(BigInt(file.bytes.byteLength));
|
|
56
|
+
hash.update(nameLength).update(name).update(size).update(file.bytes);
|
|
57
|
+
}
|
|
58
|
+
return `sha256-${hash.digest("hex")}`;
|
|
4
59
|
}
|
|
5
60
|
export function verifyPackageProvenance(content, provenance, verifySignature) {
|
|
6
61
|
const actual = checksumPackage(content);
|
package/dist/providers.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type PackageSource = {
|
|
|
5
5
|
export type LoadedPackageSource = PackageSource & {
|
|
6
6
|
root: string;
|
|
7
7
|
manifest: string;
|
|
8
|
+
manifestBytes?: Uint8Array;
|
|
8
9
|
release(): void;
|
|
9
10
|
};
|
|
10
11
|
export interface PackageProvider {
|
|
@@ -17,4 +18,5 @@ export interface PackageSourceLoader {
|
|
|
17
18
|
export declare const localProvider: PackageProvider;
|
|
18
19
|
export declare const gitProvider: PackageProvider;
|
|
19
20
|
export declare function resolveProvider(source: string, root: string): PackageSource;
|
|
21
|
+
export declare function requiresExternalNetwork(source: PackageSource): boolean;
|
|
20
22
|
export declare const nodePackageSourceLoader: PackageSourceLoader;
|
package/dist/providers.js
CHANGED
|
@@ -10,11 +10,13 @@ export const localProvider = {
|
|
|
10
10
|
}),
|
|
11
11
|
};
|
|
12
12
|
export const gitProvider = {
|
|
13
|
-
canResolve: (source) =>
|
|
13
|
+
canResolve: (source) => /^git\+(?:https?|ssh|git|file):\/\//i.test(source) ||
|
|
14
14
|
source.startsWith("git@") ||
|
|
15
15
|
source.startsWith("file://") ||
|
|
16
|
-
/^
|
|
17
|
-
|
|
16
|
+
/^ssh:\/\//i.test(source) ||
|
|
17
|
+
/^git:\/\//i.test(source) ||
|
|
18
|
+
/^https?:\/\//i.test(source) ||
|
|
19
|
+
(source.endsWith(".git") && !/^[a-z][a-z\d+.-]*:\/\//i.test(source)),
|
|
18
20
|
normalize: (source) => ({ provider: "git", source }),
|
|
19
21
|
};
|
|
20
22
|
export function resolveProvider(source, root) {
|
|
@@ -23,6 +25,22 @@ export function resolveProvider(source, root) {
|
|
|
23
25
|
throw new Error(`Unsupported package source: ${source}`);
|
|
24
26
|
return provider.normalize(source, root);
|
|
25
27
|
}
|
|
28
|
+
export function requiresExternalNetwork(source) {
|
|
29
|
+
if (source.provider !== "git")
|
|
30
|
+
return false;
|
|
31
|
+
const normalized = source.source.startsWith("git+") ? source.source.slice(4) : source.source;
|
|
32
|
+
if (/^file:\/\//i.test(normalized))
|
|
33
|
+
return false;
|
|
34
|
+
if (isAbsolute(normalized) ||
|
|
35
|
+
normalized.startsWith("./") ||
|
|
36
|
+
normalized.startsWith("../") ||
|
|
37
|
+
normalized.startsWith(".\\") ||
|
|
38
|
+
normalized.startsWith("..\\") ||
|
|
39
|
+
/^[a-z]:[\\/]/i.test(normalized))
|
|
40
|
+
return false;
|
|
41
|
+
// Any remaining Git transport/reference is treated as external unless it is an explicit file path.
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
26
44
|
export const nodePackageSourceLoader = {
|
|
27
45
|
load(source, projectRoot) {
|
|
28
46
|
const normalized = resolveProvider(source, projectRoot);
|
|
@@ -34,10 +52,12 @@ export const nodePackageSourceLoader = {
|
|
|
34
52
|
const manifestPath = isDirectory ? join(packageRoot, "package.yaml") : normalized.source;
|
|
35
53
|
if (!existsSync(manifestPath))
|
|
36
54
|
throw new Error(`Package manifest does not exist: ${manifestPath}`);
|
|
55
|
+
const manifestBytes = readFileSync(manifestPath);
|
|
37
56
|
return {
|
|
38
57
|
...normalized,
|
|
39
58
|
root: packageRoot,
|
|
40
|
-
manifest:
|
|
59
|
+
manifest: manifestBytes.toString("utf8"),
|
|
60
|
+
manifestBytes,
|
|
41
61
|
release() { },
|
|
42
62
|
};
|
|
43
63
|
}
|
|
@@ -52,10 +72,12 @@ export const nodePackageSourceLoader = {
|
|
|
52
72
|
const manifestPath = join(checkout, "package.yaml");
|
|
53
73
|
if (!existsSync(manifestPath))
|
|
54
74
|
throw new Error("Git package does not contain package.yaml.");
|
|
75
|
+
const manifestBytes = readFileSync(manifestPath);
|
|
55
76
|
return {
|
|
56
77
|
...normalized,
|
|
57
78
|
root: checkout,
|
|
58
|
-
manifest:
|
|
79
|
+
manifest: manifestBytes.toString("utf8"),
|
|
80
|
+
manifestBytes,
|
|
59
81
|
release: () => rmSync(checkout, { recursive: true, force: true }),
|
|
60
82
|
};
|
|
61
83
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ProjectProfile } from "./profile.js";
|
|
2
|
+
import type { ResourceType } from "./package-contract.js";
|
|
3
|
+
export type RecommendedResource = {
|
|
4
|
+
type: ResourceType;
|
|
5
|
+
id: string;
|
|
6
|
+
};
|
|
2
7
|
export type CapabilityRecommendation = {
|
|
3
8
|
id: string;
|
|
4
9
|
provider: string;
|
|
@@ -7,6 +12,8 @@ export type CapabilityRecommendation = {
|
|
|
7
12
|
evidence: string[];
|
|
8
13
|
permissions: string[];
|
|
9
14
|
conflicts: string[];
|
|
15
|
+
resources: RecommendedResource[];
|
|
10
16
|
};
|
|
11
17
|
export declare function recommendCapabilities(profile: ProjectProfile): CapabilityRecommendation[];
|
|
18
|
+
export declare function resourcesForCapabilities(ids: string[]): RecommendedResource[];
|
|
12
19
|
export declare function serializeRecommendations(items: CapabilityRecommendation[], selected?: string[]): string;
|