@tangle-network/agent-interface 5.6.1 → 5.7.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.
package/dist/index.d.ts CHANGED
@@ -36,6 +36,7 @@ export * from "./agent-execution-preparation.js";
36
36
  export * from "./agent-workspace-lease.js";
37
37
  export * from "./deep-freeze.js";
38
38
  export * from "./profile-diff.js";
39
+ export * from "./profile-provenance.js";
39
40
  export * from "./harness.js";
40
41
  export * from "./harness-capabilities.js";
41
42
  export * from "./profile-schema.js";
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ export * from "./agent-execution-preparation.js";
34
34
  export * from "./agent-workspace-lease.js";
35
35
  export * from "./deep-freeze.js";
36
36
  export * from "./profile-diff.js";
37
+ export * from "./profile-provenance.js";
37
38
  export * from "./harness.js";
38
39
  export * from "./harness-capabilities.js";
39
40
  export * from "./profile-schema.js";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Provenance for inline profile resources.
3
+ *
4
+ * A profile that must materialize synchronously carries its skills as exact
5
+ * inline bytes. The record beside each one names the file and commit the bytes
6
+ * came from and their SHA-256, so an edit to either the inline copy or the
7
+ * source shows up as a mismatch instead of a silent change of behavior.
8
+ * Records live at `extensions[<profile name>].resourceProvenance`.
9
+ */
10
+ import type { AgentProfile, AgentProfileResourceRef } from "./agent-profile.js";
11
+ /** One file at one commit. */
12
+ export interface AgentProfileResourceSource {
13
+ /** `owner/repo`. */
14
+ repository: string;
15
+ /** Full commit SHA: a branch or tag can move under the recorded bytes. */
16
+ ref: string;
17
+ path: string;
18
+ }
19
+ /** The record a profile keeps for one inline resource. */
20
+ export interface AgentProfileResourceProvenance extends AgentProfileResourceSource {
21
+ /** Name of the inline resource the bytes were mounted as. */
22
+ name: string;
23
+ /** Lowercase hex SHA-256 of the content's UTF-8 bytes. */
24
+ sha256: string;
25
+ }
26
+ export type AgentProfileInlineResource = Extract<AgentProfileResourceRef, {
27
+ kind: "inline";
28
+ }>;
29
+ /** Reads the exact bytes of a source file, e.g. `git show <ref>:<path>` or the GitHub contents API. */
30
+ export type ReadAgentProfileResourceSource = (source: AgentProfileResourceSource) => Promise<string>;
31
+ /** Mount `content` as an inline skill and return the provenance record to keep beside it. */
32
+ export declare function mountSkill(source: AgentProfileResourceSource & {
33
+ name: string;
34
+ }, content: string): {
35
+ skill: AgentProfileInlineResource;
36
+ provenance: AgentProfileResourceProvenance;
37
+ };
38
+ /**
39
+ * Every reason a profile's provenance records do not hold; empty when they do.
40
+ *
41
+ * Each record must be pinned to a commit and name inline resources whose bytes
42
+ * hash to it. Inline resources without a record are not checked: a record is
43
+ * the producer's claim, and only claims are verified. With `read`, the source file at that commit must hash to it too,
44
+ * which catches a record that was rewritten together with the inline copy.
45
+ */
46
+ export declare function verifyResourceProvenance(profile: AgentProfile, options?: {
47
+ namespace?: string;
48
+ read?: ReadAgentProfileResourceSource;
49
+ }): Promise<string[]>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Provenance for inline profile resources.
3
+ *
4
+ * A profile that must materialize synchronously carries its skills as exact
5
+ * inline bytes. The record beside each one names the file and commit the bytes
6
+ * came from and their SHA-256, so an edit to either the inline copy or the
7
+ * source shows up as a mismatch instead of a silent change of behavior.
8
+ * Records live at `extensions[<profile name>].resourceProvenance`.
9
+ */
10
+ import { gitObjectSchema, sha256Utf8 } from "./agent-candidate-schema-common.js";
11
+ /** Mount `content` as an inline skill and return the provenance record to keep beside it. */
12
+ export function mountSkill(source, content) {
13
+ if (!gitObjectSchema.safeParse(source.ref).success) {
14
+ throw new Error(`mountSkill ${source.name}: ref must be a full commit SHA, got "${source.ref}"`);
15
+ }
16
+ return {
17
+ skill: { kind: "inline", name: source.name, content },
18
+ provenance: {
19
+ name: source.name,
20
+ repository: source.repository,
21
+ ref: source.ref,
22
+ path: source.path,
23
+ sha256: contentSha256(content),
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * Every reason a profile's provenance records do not hold; empty when they do.
29
+ *
30
+ * Each record must be pinned to a commit and name inline resources whose bytes
31
+ * hash to it. Inline resources without a record are not checked: a record is
32
+ * the producer's claim, and only claims are verified. With `read`, the source file at that commit must hash to it too,
33
+ * which catches a record that was rewritten together with the inline copy.
34
+ */
35
+ export async function verifyResourceProvenance(profile, options = {}) {
36
+ const namespace = options.namespace ?? profile.name;
37
+ if (namespace === undefined)
38
+ return ["profile has no name to locate its provenance records"];
39
+ const records = profile.extensions?.[namespace]?.resourceProvenance;
40
+ if (records === undefined)
41
+ return [];
42
+ if (!Array.isArray(records))
43
+ return [`extensions.${namespace}.resourceProvenance must be an array`];
44
+ const inline = inlineResourcesByName(profile);
45
+ const problems = [];
46
+ for (const [index, record] of records.entries()) {
47
+ const at = `extensions.${namespace}.resourceProvenance[${index}]`;
48
+ if (!isProvenanceRecord(record)) {
49
+ problems.push(`${at}: needs string name, repository, ref, path and a 64-hex sha256`);
50
+ continue;
51
+ }
52
+ if (!gitObjectSchema.safeParse(record.ref).success) {
53
+ problems.push(`${at} (${record.name}): ref "${record.ref}" is not a full commit SHA`);
54
+ }
55
+ const mounted = inline.get(record.name) ?? [];
56
+ if (mounted.length === 0) {
57
+ problems.push(`${at} (${record.name}): no inline resource has this name`);
58
+ }
59
+ for (const content of mounted) {
60
+ const actual = contentSha256(content);
61
+ if (actual !== record.sha256) {
62
+ problems.push(`${at} (${record.name}): inline bytes hash to ${actual}, record says ${record.sha256}`);
63
+ }
64
+ }
65
+ if (options.read !== undefined) {
66
+ let source;
67
+ try {
68
+ source = await options.read(record);
69
+ }
70
+ catch (error) {
71
+ problems.push(`${at} (${record.name}): cannot read ${record.repository}@${record.ref}:${record.path}: ${error instanceof Error ? error.message : String(error)}`);
72
+ continue;
73
+ }
74
+ const actual = contentSha256(source);
75
+ if (actual !== record.sha256) {
76
+ problems.push(`${at} (${record.name}): ${record.repository}@${record.ref}:${record.path} hashes to ${actual}, record says ${record.sha256}`);
77
+ }
78
+ }
79
+ }
80
+ return problems;
81
+ }
82
+ function contentSha256(content) {
83
+ return sha256Utf8(content).slice("sha256:".length);
84
+ }
85
+ function isProvenanceRecord(value) {
86
+ if (value === null || typeof value !== "object")
87
+ return false;
88
+ const record = value;
89
+ return (["name", "repository", "ref", "path"].every((key) => typeof record[key] === "string" && record[key] !== "") &&
90
+ typeof record.sha256 === "string" &&
91
+ /^[a-f0-9]{64}$/.test(record.sha256));
92
+ }
93
+ /**
94
+ * Inline contents by name. A record names a resource but not its kind, and
95
+ * `mountSkill` records skills, so a skill of that name wins; other kinds are
96
+ * matched only when no skill has the name, so a same-named command or file
97
+ * cannot fail a skill's record.
98
+ */
99
+ function inlineResourcesByName(profile) {
100
+ const resources = profile.resources;
101
+ const others = [
102
+ ...(resources?.files?.map((mount) => mount.resource) ?? []),
103
+ ...(resources?.tools ?? []),
104
+ ...(resources?.agents ?? []),
105
+ ...(resources?.commands ?? []),
106
+ typeof resources?.instructions === "object" ? resources.instructions : undefined,
107
+ ];
108
+ const skills = byName(resources?.skills ?? []);
109
+ for (const [name, contents] of byName(others)) {
110
+ if (!skills.has(name))
111
+ skills.set(name, contents);
112
+ }
113
+ return skills;
114
+ }
115
+ function byName(refs) {
116
+ const map = new Map();
117
+ for (const ref of refs) {
118
+ if (ref?.kind !== "inline")
119
+ continue;
120
+ map.set(ref.name, [...(map.get(ref.name) ?? []), ref.content]);
121
+ }
122
+ return map;
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "5.6.1",
3
+ "version": "5.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",