@tangle-network/agent-interface 5.6.0 → 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/harness.d.ts +11 -20
- package/dist/harness.js +12 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/profile-provenance.d.ts +49 -0
- package/dist/profile-provenance.js +123 -0
- package/package.json +1 -1
package/dist/harness.d.ts
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Recognized execution identities, not a deployment's list of installed runners.
|
|
4
|
+
* A profile's authored harness remains part of its canonical identity; execution
|
|
5
|
+
* receipts record authored identity, effective identity and selected harness
|
|
6
|
+
* separately. Retiring an executable must not invalidate historical profiles,
|
|
7
|
+
* sessions or receipts. Availability is admitted against the selected deployment.
|
|
4
8
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the single shared enum every layer references instead of keeping its own copy (session control,
|
|
10
|
-
* the profile materializer, the cli-bridge backends, VB profile specs).
|
|
11
|
-
*
|
|
12
|
-
* `cli-base` is the router-backed mode — a plain multi-turn router call (a reviewer, a cheap judge,
|
|
13
|
-
* a one-shot) with no full coding-agent harness. The rest are full agentic harnesses run in a
|
|
14
|
-
* sandbox or locally via the CLI bridge.
|
|
15
|
-
*
|
|
16
|
-
* `forge` (tailcallhq/forgecode) and `cursor` (cursor-agent) are multi-provider CLI harnesses with
|
|
17
|
-
* no vendor lock, so they carry no entry in the capability tables and resolve as router-backed.
|
|
18
|
-
*
|
|
19
|
-
* `prime` (PrimeIntellect-ai/prime-agent) is the prime fork of the pi line — the RLM
|
|
20
|
-
* IPython-kernel harness. It is a distinct harness from `pi`: the fork's wire protocol has
|
|
21
|
-
* diverged (its daemon rejects pi-line clients), so the two are not interchangeable at run time.
|
|
9
|
+
* `cli-base` names router-backed execution without a full coding-agent harness.
|
|
10
|
+
* `prime` is a distinct Pi-line fork with a different native wire protocol; its
|
|
11
|
+
* sessions are not interchangeable with Pi sessions. A shared identity does not
|
|
12
|
+
* assert protocol, model, profile-control or native-resume compatibility.
|
|
22
13
|
*/
|
|
23
|
-
export type HarnessType = "claude-code" | "nanoclaw" | "codex" | "opencode" | "kimi-code" | "pi" | "prime" | "gemini" | "hermes" | "openclaw" | "amp" | "factory-droids" | "forge" | "cursor" | "acp" | "cli-base";
|
|
24
|
-
/** Runtime validator for {@link HarnessType}. Kept in lockstep with the type by the drift guard below. */
|
|
25
14
|
export declare const harnessTypeSchema: z.ZodEnum<{
|
|
26
15
|
acp: "acp";
|
|
27
16
|
amp: "amp";
|
|
@@ -40,3 +29,5 @@ export declare const harnessTypeSchema: z.ZodEnum<{
|
|
|
40
29
|
pi: "pi";
|
|
41
30
|
prime: "prime";
|
|
42
31
|
}>;
|
|
32
|
+
/** One declaration owns runtime validation and the compile-time vocabulary. */
|
|
33
|
+
export type HarnessType = z.infer<typeof harnessTypeSchema>;
|
package/dist/harness.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Recognized execution identities, not a deployment's list of installed runners.
|
|
4
|
+
* A profile's authored harness remains part of its canonical identity; execution
|
|
5
|
+
* receipts record authored identity, effective identity and selected harness
|
|
6
|
+
* separately. Retiring an executable must not invalidate historical profiles,
|
|
7
|
+
* sessions or receipts. Availability is admitted against the selected deployment.
|
|
8
|
+
*
|
|
9
|
+
* `cli-base` names router-backed execution without a full coding-agent harness.
|
|
10
|
+
* `prime` is a distinct Pi-line fork with a different native wire protocol; its
|
|
11
|
+
* sessions are not interchangeable with Pi sessions. A shared identity does not
|
|
12
|
+
* assert protocol, model, profile-control or native-resume compatibility.
|
|
13
|
+
*/
|
|
3
14
|
export const harnessTypeSchema = z.enum([
|
|
4
15
|
"claude-code",
|
|
5
16
|
"nanoclaw",
|
|
@@ -18,5 +29,3 @@ export const harnessTypeSchema = z.enum([
|
|
|
18
29
|
"acp",
|
|
19
30
|
"cli-base",
|
|
20
31
|
]);
|
|
21
|
-
const _harnessSchemaMatchesType = true;
|
|
22
|
-
void _harnessSchemaMatchesType;
|
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
|
+
}
|