@warpgogol/werkstatt-shared 0.15.0 → 0.16.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/package.json +18 -1
- package/src/agent/api-catalog.ts +14 -1
- package/src/agent/guidance.ts +52 -0
- package/src/agent/health.ts +68 -0
- package/src/agent/index.ts +5 -1
- package/src/agent/manifest.ts +70 -7
- package/src/agent/openapi.ts +97 -4
- package/src/agent/receipt.ts +123 -0
- package/src/agent/search.test.ts +3 -0
- package/src/agent/search.ts +25 -4
- package/src/agent/tests/api-catalog.test.ts +6 -0
- package/src/agent/tests/ard-catalog.test.ts +9 -0
- package/src/agent/tests/manifest.test.ts +71 -0
- package/src/agent/tests/mcp-card.test.ts +4 -0
- package/src/agent/tests/openapi.test.ts +117 -0
- package/src/component/index.ts +3 -1
- package/src/fingerprint/index.ts +3 -1
- package/src/integration/idempotency.ts +88 -0
- package/src/integration/index.ts +4 -0
- package/src/integration/tests/idempotency.test.ts +81 -0
- package/src/kernel/drift-guard.ts +166 -0
- package/src/kernel/index.ts +8 -1
- package/src/kernel/tests/drift-guard.test.ts +112 -0
- package/src/kernel/tests/workpiece-env.test.ts +44 -0
- package/src/kernel/types.ts +63 -4
- package/src/kernel/workpiece-env.ts +48 -0
- package/src/middleware/access-protection.ts +3 -0
- package/src/middleware/language-redirect.ts +3 -0
- package/src/observability/index.ts +8 -0
- package/src/observability/pipeline-log.ts +81 -0
- package/src/ontology/capabilities/lead.prepare.yaml +44 -0
- package/src/ontology/capabilities/lead.submit.yaml +20 -3
- package/src/ontology/operations/index.ts +6 -0
- package/src/ontology/operations/notausgang.ts +44 -0
- package/src/ontology/schemas/capability.ts +55 -3
- package/src/passport/claim-sign.ts +120 -0
- package/src/passport/index.ts +3 -0
- package/src/plugin/plugin-contract.ts +1 -1
- package/src/semantic/canonical-uri.ts +28 -8
- package/src/semantic/llms.ts +10 -2
- package/src/semantic/tests/canonical-uri.test.ts +18 -6
- package/src/signing/index.ts +3 -1
- package/src/stack/run-tool.ts +2 -4
package/src/agent/search.ts
CHANGED
|
@@ -20,6 +20,7 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
20
20
|
<item>RFC-1097: sweep — werkstatt-engine clean
|
|
21
21
|
|
|
22
22
|
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
|
|
23
|
+
<item>RFC-1115: SearchChunk gains citation fields (title, canonicalUrl, updatedAt); SearchResult flattened to { title, canonicalUrl, updatedAt, lang, type, score, text }; SEARCH_MANIFEST_SCHEMA_VERSION "1" → "2".</item>
|
|
23
24
|
</CHANGE_SUMMARY>
|
|
24
25
|
*/
|
|
25
26
|
|
|
@@ -44,8 +45,8 @@ export const SEARCH_DEFAULT_TOP_K = 5;
|
|
|
44
45
|
/** Maximum character length for a chunk's `text` field (truncated before embedding). */
|
|
45
46
|
export const SEARCH_CHUNK_TEXT_MAX_LENGTH = 2000;
|
|
46
47
|
|
|
47
|
-
/** Schema version for the search manifest. */
|
|
48
|
-
export const SEARCH_MANIFEST_SCHEMA_VERSION = "
|
|
48
|
+
/** Schema version for the search manifest. RFC-1115: "2" adds citation fields to chunks. */
|
|
49
|
+
export const SEARCH_MANIFEST_SCHEMA_VERSION = "2";
|
|
49
50
|
|
|
50
51
|
/** Static asset path for the generated search manifest. */
|
|
51
52
|
export const SEARCH_MANIFEST_PATH = "public/api/agent/v1/search-manifest.json";
|
|
@@ -86,6 +87,12 @@ export interface SearchChunk {
|
|
|
86
87
|
heading: string | null;
|
|
87
88
|
/** Body text extracted from the block, truncated to SEARCH_CHUNK_TEXT_MAX_LENGTH. */
|
|
88
89
|
text: string;
|
|
90
|
+
/** RFC-1115: display title for citations (from heading; null when absent). */
|
|
91
|
+
title: string | null;
|
|
92
|
+
/** RFC-1115: absolute canonical URL (site origin + url) — citable without a follow-up fetch. */
|
|
93
|
+
canonicalUrl: string;
|
|
94
|
+
/** RFC-1115: source-backed update stamp YYYY-MM-DD (RFC-0317); null when unknown. */
|
|
95
|
+
updatedAt: string | null;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
/** Static search manifest — build-time output written to public/api/agent/v1/search-manifest.json. */
|
|
@@ -117,11 +124,25 @@ export interface SearchQuery {
|
|
|
117
124
|
topK?: number;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* RFC-1115: a single search result — flattened citation shape.
|
|
129
|
+
* Carries everything an agent needs to cite without a follow-up fetch.
|
|
130
|
+
*/
|
|
121
131
|
export interface SearchResult {
|
|
122
|
-
chunk
|
|
132
|
+
/** Display title (from chunk heading; null when absent). */
|
|
133
|
+
title: string | null;
|
|
134
|
+
/** Absolute canonical URL of the chunk's page or resource. */
|
|
135
|
+
canonicalUrl: string;
|
|
136
|
+
/** Source-backed update stamp YYYY-MM-DD (RFC-0317); null when unknown. */
|
|
137
|
+
updatedAt: string | null;
|
|
138
|
+
/** ISO 639-1 language code. */
|
|
139
|
+
lang: string;
|
|
140
|
+
/** Content type of the underlying chunk. */
|
|
141
|
+
type: SearchChunkType;
|
|
123
142
|
/** Cosine similarity score (0..1, higher = more similar). */
|
|
124
143
|
score: number;
|
|
144
|
+
/** Body text of the chunk. */
|
|
145
|
+
text: string;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
/** Response from the search endpoint. */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
</MODULE_MAP>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0783: initial API Catalog projection tests.</item>
|
|
11
|
+
<item>RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era</item>
|
|
11
12
|
</CHANGE_SUMMARY>
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -17,6 +18,7 @@ import { buildAgentSurfaceManifest } from "../manifest.ts";
|
|
|
17
18
|
|
|
18
19
|
test("buildApiCatalog: empty manifest yields service-meta + service-doc links", () => {
|
|
19
20
|
const manifest = buildAgentSurfaceManifest({
|
|
21
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
20
22
|
site: "s",
|
|
21
23
|
baseUrl: "https://s.example",
|
|
22
24
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -34,6 +36,7 @@ test("buildApiCatalog: empty manifest yields service-meta + service-doc links",
|
|
|
34
36
|
|
|
35
37
|
test("buildApiCatalog: one knowledge ref → one item link", () => {
|
|
36
38
|
const manifest = buildAgentSurfaceManifest({
|
|
39
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
37
40
|
site: "s",
|
|
38
41
|
baseUrl: "https://s.example",
|
|
39
42
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -51,6 +54,7 @@ test("buildApiCatalog: one knowledge ref → one item link", () => {
|
|
|
51
54
|
|
|
52
55
|
test("buildApiCatalog: mcp interface adds service-desc + service links", () => {
|
|
53
56
|
const manifest = buildAgentSurfaceManifest({
|
|
57
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
54
58
|
site: "s",
|
|
55
59
|
baseUrl: "https://s.example",
|
|
56
60
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -69,6 +73,7 @@ test("buildApiCatalog: determinism — same input produces identical output", ()
|
|
|
69
73
|
site: "s",
|
|
70
74
|
baseUrl: "https://s.example",
|
|
71
75
|
languages: { default: "de", supported: ["de"] },
|
|
76
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
72
77
|
knowledge: [
|
|
73
78
|
{ domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" },
|
|
74
79
|
{ domain: "team", url: "/api/agent/v1/team.json", schema: "gogol.agent.knowledge/team@1" },
|
|
@@ -82,6 +87,7 @@ test("buildApiCatalog: determinism — same input produces identical output", ()
|
|
|
82
87
|
|
|
83
88
|
test("buildApiCatalog: links are sorted by href within each relation", () => {
|
|
84
89
|
const manifest = buildAgentSurfaceManifest({
|
|
90
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
85
91
|
site: "s",
|
|
86
92
|
baseUrl: "https://s.example",
|
|
87
93
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
</MODULE_CONTRACT>
|
|
6
6
|
<CHANGE_SUMMARY>
|
|
7
7
|
<item>Initial ARD ai-catalog.json projection tests.</item>
|
|
8
|
+
<item>RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era</item>
|
|
8
9
|
</CHANGE_SUMMARY>
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -14,6 +15,7 @@ import { buildAgentSurfaceManifest } from "../manifest.ts";
|
|
|
14
15
|
|
|
15
16
|
test("buildArdCatalog: produces specVersion, host, and entries array", () => {
|
|
16
17
|
const manifest = buildAgentSurfaceManifest({
|
|
18
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
17
19
|
site: "s",
|
|
18
20
|
baseUrl: "https://s.example",
|
|
19
21
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -27,6 +29,7 @@ test("buildArdCatalog: produces specVersion, host, and entries array", () => {
|
|
|
27
29
|
|
|
28
30
|
test("buildArdCatalog: each entry has identifier, displayName, type, url, representativeQueries", () => {
|
|
29
31
|
const manifest = buildAgentSurfaceManifest({
|
|
32
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
30
33
|
site: "s",
|
|
31
34
|
baseUrl: "https://s.example",
|
|
32
35
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -48,6 +51,7 @@ test("buildArdCatalog: each entry has identifier, displayName, type, url, repres
|
|
|
48
51
|
|
|
49
52
|
test("buildArdCatalog: MCP interface adds MCP server entry", () => {
|
|
50
53
|
const manifest = buildAgentSurfaceManifest({
|
|
54
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
51
55
|
site: "s",
|
|
52
56
|
baseUrl: "https://s.example",
|
|
53
57
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -62,6 +66,7 @@ test("buildArdCatalog: MCP interface adds MCP server entry", () => {
|
|
|
62
66
|
|
|
63
67
|
test("buildArdCatalog: knowledge refs produce entries", () => {
|
|
64
68
|
const manifest = buildAgentSurfaceManifest({
|
|
69
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
65
70
|
site: "s",
|
|
66
71
|
baseUrl: "https://s.example",
|
|
67
72
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -78,6 +83,7 @@ test("buildArdCatalog: knowledge refs produce entries", () => {
|
|
|
78
83
|
|
|
79
84
|
test("buildArdCatalog: entries are sorted by identifier", () => {
|
|
80
85
|
const manifest = buildAgentSurfaceManifest({
|
|
86
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
81
87
|
site: "s",
|
|
82
88
|
baseUrl: "https://s.example",
|
|
83
89
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -97,6 +103,7 @@ test("buildArdCatalog: determinism — same input produces identical output", ()
|
|
|
97
103
|
site: "s",
|
|
98
104
|
baseUrl: "https://s.example",
|
|
99
105
|
languages: { default: "de", supported: ["de"] },
|
|
106
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
100
107
|
knowledge: [
|
|
101
108
|
{ domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" },
|
|
102
109
|
],
|
|
@@ -109,6 +116,7 @@ test("buildArdCatalog: determinism — same input produces identical output", ()
|
|
|
109
116
|
|
|
110
117
|
test("buildArdCatalog: host identifier uses did:web format with domain", () => {
|
|
111
118
|
const manifest = buildAgentSurfaceManifest({
|
|
119
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
112
120
|
site: "warpgogol-com",
|
|
113
121
|
baseUrl: "https://warpgogol.com",
|
|
114
122
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -120,6 +128,7 @@ test("buildArdCatalog: host identifier uses did:web format with domain", () => {
|
|
|
120
128
|
|
|
121
129
|
test("buildArdCatalog: default hostDisplayName falls back to site name", () => {
|
|
122
130
|
const manifest = buildAgentSurfaceManifest({
|
|
131
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
123
132
|
site: "my-site",
|
|
124
133
|
baseUrl: "https://example.com",
|
|
125
134
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0286: initial builder tests.</item>
|
|
11
11
|
<item>RFC-0954: add search interface tests.</item>
|
|
12
|
+
<item>RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era</item>
|
|
13
|
+
<item>RFC-1114: add a2a interface + supportedProtocols tests (AC-5).</item>
|
|
14
|
+
<item>RFC-1114: minimal real A2A endpoint + honest agent card</item>
|
|
12
15
|
</CHANGE_SUMMARY>
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { test, expect } from "vitest";
|
|
16
19
|
import {
|
|
20
|
+
AGENT_SURFACE_SCHEMA_ID,
|
|
17
21
|
AGENT_SURFACE_VERSION,
|
|
18
22
|
buildAgentSurfaceManifest,
|
|
19
23
|
computeAgentManifestContentHash,
|
|
@@ -25,6 +29,7 @@ const baseInput = {
|
|
|
25
29
|
site: "warpgogol-com",
|
|
26
30
|
baseUrl: "https://warpgogol.com/",
|
|
27
31
|
languages: { default: "de", supported: ["uk", "de"] },
|
|
32
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
test("buildAgentSurfaceManifest: strips trailing slash and sorts supported languages", () => {
|
|
@@ -39,6 +44,7 @@ test("buildAgentSurfaceManifest: safe default — empty knowledge/actions, null
|
|
|
39
44
|
expect(m.actions).toEqual([]);
|
|
40
45
|
expect(m.interfaces.openapi).toBe(null);
|
|
41
46
|
expect(m.interfaces.mcp).toBe(null);
|
|
47
|
+
expect(m.interfaces.a2a).toBe(null);
|
|
42
48
|
expect(m.interfaces.twins).toBe(null);
|
|
43
49
|
expect(m.interfaces.search).toBe(null);
|
|
44
50
|
expect(m.proof).toBe(null);
|
|
@@ -149,3 +155,68 @@ test("computeSignedContentHash: matches computeAgentManifestContentHash for same
|
|
|
149
155
|
const doc = { a: 1, b: 2, contentHash: "x", proof: null };
|
|
150
156
|
expect(computeSignedContentHash(doc)).toBe(computeAgentManifestContentHash(doc));
|
|
151
157
|
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// RFC-1113: discovery metadata fields
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
test("buildAgentSurfaceManifest: emits $schema, generatedAt, supportedProtocols", () => {
|
|
164
|
+
const m = buildAgentSurfaceManifest(baseInput);
|
|
165
|
+
expect(m.$schema).toBe(AGENT_SURFACE_SCHEMA_ID);
|
|
166
|
+
expect(m.generatedAt).toBe("2026-01-01T00:00:00.000Z");
|
|
167
|
+
// Unconditional discovery artifacts only — no interfaces were provided.
|
|
168
|
+
expect(m.supportedProtocols).toEqual(["agent-skills", "ard", "dns-aid", "linkset", "llms"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("buildAgentSurfaceManifest: supportedProtocols follows emitted interfaces", () => {
|
|
172
|
+
const m = buildAgentSurfaceManifest({
|
|
173
|
+
...baseInput,
|
|
174
|
+
hasTwins: true,
|
|
175
|
+
openapiUrl: "/.well-known/agent.openapi.json",
|
|
176
|
+
mcp: { url: "/api/agent/mcp", protocolVersion: "2026-07-28" },
|
|
177
|
+
search: { url: "/api/agent/search", model: "@cf/baai/bge-m3", dimensions: 1024 },
|
|
178
|
+
});
|
|
179
|
+
expect(m.supportedProtocols).toEqual([
|
|
180
|
+
"agent-skills",
|
|
181
|
+
"ard",
|
|
182
|
+
"dns-aid",
|
|
183
|
+
"linkset",
|
|
184
|
+
"llms",
|
|
185
|
+
"mcp",
|
|
186
|
+
"openapi",
|
|
187
|
+
"search",
|
|
188
|
+
"twins",
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// RFC-1114: a2a interface (AC-5)
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
test("buildAgentSurfaceManifest: populates interfaces.a2a and lists a2a in supportedProtocols", () => {
|
|
197
|
+
const m = buildAgentSurfaceManifest({
|
|
198
|
+
...baseInput,
|
|
199
|
+
a2a: { url: "/api/agent/a2a", protocolVersion: "1.0" },
|
|
200
|
+
});
|
|
201
|
+
expect(m.interfaces.a2a).toEqual({ url: "/api/agent/a2a", protocolVersion: "1.0" });
|
|
202
|
+
expect(m.supportedProtocols).toContain("a2a");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("buildAgentSurfaceManifest: a2a absent from supportedProtocols when no a2a interface", () => {
|
|
206
|
+
const m = buildAgentSurfaceManifest(baseInput);
|
|
207
|
+
expect(m.interfaces.a2a).toBe(null);
|
|
208
|
+
expect(m.supportedProtocols).not.toContain("a2a");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("buildAgentSurfaceManifest: generatedAt does not change contentHash (AC-3)", () => {
|
|
212
|
+
const a = buildAgentSurfaceManifest(baseInput);
|
|
213
|
+
const b = buildAgentSurfaceManifest({ ...baseInput, generatedAt: "2026-12-31T23:59:59.000Z" });
|
|
214
|
+
expect(a.contentHash).toBe(b.contentHash);
|
|
215
|
+
expect(a.generatedAt).not.toBe(b.generatedAt);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("computeAgentManifestContentHash: excludes generatedAt", () => {
|
|
219
|
+
const doc = { a: 1, generatedAt: "2026-01-01T00:00:00Z" };
|
|
220
|
+
const other = { a: 1, generatedAt: "2026-06-06T06:06:06Z" };
|
|
221
|
+
expect(computeAgentManifestContentHash(doc)).toBe(computeAgentManifestContentHash(other));
|
|
222
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
</MODULE_MAP>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0783: initial MCP Server Card projection tests.</item>
|
|
11
|
+
<item>RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era</item>
|
|
11
12
|
</CHANGE_SUMMARY>
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -17,6 +18,7 @@ import { buildAgentSurfaceManifest } from "../manifest.ts";
|
|
|
17
18
|
|
|
18
19
|
test("buildMcpServerCard: null mcp → returns null", () => {
|
|
19
20
|
const manifest = buildAgentSurfaceManifest({
|
|
21
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
20
22
|
site: "s",
|
|
21
23
|
baseUrl: "https://s.example",
|
|
22
24
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -26,6 +28,7 @@ test("buildMcpServerCard: null mcp → returns null", () => {
|
|
|
26
28
|
|
|
27
29
|
test("buildMcpServerCard: mcp interface → card with correct fields", () => {
|
|
28
30
|
const manifest = buildAgentSurfaceManifest({
|
|
31
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
29
32
|
site: "warpgogol",
|
|
30
33
|
baseUrl: "https://warpgogol.com",
|
|
31
34
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -49,6 +52,7 @@ test("buildMcpServerCard: determinism — same input produces identical output",
|
|
|
49
52
|
site: "s",
|
|
50
53
|
baseUrl: "https://s.example",
|
|
51
54
|
languages: { default: "de", supported: ["de"] },
|
|
55
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
52
56
|
mcp: { url: "/api/agent/mcp", protocolVersion: "2025-06-18" },
|
|
53
57
|
};
|
|
54
58
|
const a = buildMcpServerCard(buildAgentSurfaceManifest(input));
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0289: initial OpenAPI formatter tests.</item>
|
|
11
11
|
<item>RFC-0954: add search path projection tests.</item>
|
|
12
|
+
<item>RFC-1113: align agent discovery surface with RFC 9727 and MCP 2026-07-28 stateless era</item>
|
|
12
13
|
</CHANGE_SUMMARY>
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -18,6 +19,7 @@ import { buildAgentSurfaceManifest } from "../manifest.ts";
|
|
|
18
19
|
|
|
19
20
|
test("formatAgentOpenApi: empty manifest yields a valid document with no paths", () => {
|
|
20
21
|
const manifest = buildAgentSurfaceManifest({
|
|
22
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
21
23
|
site: "s",
|
|
22
24
|
baseUrl: "https://s.example",
|
|
23
25
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -33,6 +35,7 @@ test("formatAgentOpenApi: empty manifest yields a valid document with no paths",
|
|
|
33
35
|
|
|
34
36
|
test("formatAgentOpenApi: one knowledge ref → one GET path + one component schema", () => {
|
|
35
37
|
const manifest = buildAgentSurfaceManifest({
|
|
38
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
36
39
|
site: "s",
|
|
37
40
|
baseUrl: "https://s.example",
|
|
38
41
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -61,6 +64,7 @@ test("formatAgentOpenApi: one action ref → one POST path with verbatim input/o
|
|
|
61
64
|
properties: { accepted: { type: "boolean" as const } },
|
|
62
65
|
};
|
|
63
66
|
const manifest = buildAgentSurfaceManifest({
|
|
67
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
64
68
|
site: "s",
|
|
65
69
|
baseUrl: "https://s.example",
|
|
66
70
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -84,10 +88,25 @@ test("formatAgentOpenApi: one action ref → one POST path with verbatim input/o
|
|
|
84
88
|
expect(doc.components.schemas["lead.submit-input"]).toEqual(inputSchema);
|
|
85
89
|
expect(doc.components.schemas["lead.submit-output"]).toEqual(outputSchema);
|
|
86
90
|
expect(op!.requestBody!.content["application/json"].schema).toEqual(inputSchema);
|
|
91
|
+
|
|
92
|
+
// RFC-1112: Idempotency-Key header parameter.
|
|
93
|
+
const idemParam = op!.parameters!.find((p) => p.name === "Idempotency-Key");
|
|
94
|
+
expect(idemParam).toBeTruthy();
|
|
95
|
+
expect(idemParam!.in).toBe("header");
|
|
96
|
+
expect(idemParam!.required).toBe(false);
|
|
97
|
+
|
|
98
|
+
// RFC-1112: problem+json error responses + shared problem-details schema.
|
|
99
|
+
expect(doc.components.schemas["problem-details"]).toBeTruthy();
|
|
100
|
+
for (const status of ["400", "409", "413", "422", "429", "502"]) {
|
|
101
|
+
const res = op!.responses[status];
|
|
102
|
+
expect(res, `missing ${status} response`).toBeTruthy();
|
|
103
|
+
expect(res!.content?.["application/problem+json"]).toBeTruthy();
|
|
104
|
+
}
|
|
87
105
|
});
|
|
88
106
|
|
|
89
107
|
test("formatAgentOpenApi: an action ref without a matching capability schema is skipped defensively", () => {
|
|
90
108
|
const manifest = buildAgentSurfaceManifest({
|
|
109
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
91
110
|
site: "s",
|
|
92
111
|
baseUrl: "https://s.example",
|
|
93
112
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -107,6 +126,7 @@ test("formatAgentOpenApi: an action ref without a matching capability schema is
|
|
|
107
126
|
|
|
108
127
|
test("formatAgentOpenApi: mcp interface present → x-gogol-mcp is set", () => {
|
|
109
128
|
const manifest = buildAgentSurfaceManifest({
|
|
129
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
110
130
|
site: "s",
|
|
111
131
|
baseUrl: "https://s.example",
|
|
112
132
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -118,6 +138,7 @@ test("formatAgentOpenApi: mcp interface present → x-gogol-mcp is set", () => {
|
|
|
118
138
|
|
|
119
139
|
test("formatAgentOpenApi: search interface present → /api/agent/search GET path with dynamic lang enum", () => {
|
|
120
140
|
const manifest = buildAgentSurfaceManifest({
|
|
141
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
121
142
|
site: "s",
|
|
122
143
|
baseUrl: "https://s.example",
|
|
123
144
|
languages: { default: "de", supported: ["de", "uk"] },
|
|
@@ -136,6 +157,7 @@ test("formatAgentOpenApi: search interface present → /api/agent/search GET pat
|
|
|
136
157
|
|
|
137
158
|
test("formatAgentOpenApi: search interface absent → no search path", () => {
|
|
138
159
|
const manifest = buildAgentSurfaceManifest({
|
|
160
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
139
161
|
site: "s",
|
|
140
162
|
baseUrl: "https://s.example",
|
|
141
163
|
languages: { default: "de", supported: ["de"] },
|
|
@@ -144,3 +166,98 @@ test("formatAgentOpenApi: search interface absent → no search path", () => {
|
|
|
144
166
|
expect(doc.paths["/api/agent/search"]).toBe(undefined);
|
|
145
167
|
expect(doc.components.schemas["search-response"]).toBe(undefined);
|
|
146
168
|
});
|
|
169
|
+
|
|
170
|
+
test("formatAgentOpenApi: agentGuidance renders into the operation description (RFC-1115 AC-4)", () => {
|
|
171
|
+
const manifest = buildAgentSurfaceManifest({
|
|
172
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
173
|
+
site: "s",
|
|
174
|
+
baseUrl: "https://s.example",
|
|
175
|
+
languages: { default: "de", supported: ["de"] },
|
|
176
|
+
actions: [
|
|
177
|
+
{
|
|
178
|
+
id: "lead.submit",
|
|
179
|
+
url: "/api/agent/actions/lead.submit",
|
|
180
|
+
title: { de: "Anfrage senden" },
|
|
181
|
+
inputSchemaRef: "#/components/schemas/lead.submit-input",
|
|
182
|
+
entitlement: "agent.actions",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
const doc = formatAgentOpenApi(manifest, [
|
|
187
|
+
{
|
|
188
|
+
id: "lead.submit",
|
|
189
|
+
input: { type: "object" },
|
|
190
|
+
output: { type: "object" },
|
|
191
|
+
description: { de: "Reicht eine Kontaktanfrage ein." },
|
|
192
|
+
agentGuidance: {
|
|
193
|
+
sideEffect: "write",
|
|
194
|
+
whenToUse: "ein Besucher das Gewerbe kontaktieren will.",
|
|
195
|
+
whenNotToUse: "die Antwort bereits auf der Seite steht.",
|
|
196
|
+
retrySemantics: "mit demselben Idempotency-Key sicher wiederholbar.",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
]);
|
|
200
|
+
const op = doc.paths["/api/agent/actions/lead.submit"]?.post;
|
|
201
|
+
expect(op).toBeTruthy();
|
|
202
|
+
expect(op!.description).toContain("Reicht eine Kontaktanfrage ein.");
|
|
203
|
+
expect(op!.description).toContain("Use this when ein Besucher das Gewerbe kontaktieren will.");
|
|
204
|
+
expect(op!.description).toContain("Do not use this when die Antwort bereits auf der Seite steht.");
|
|
205
|
+
expect(op!.description).toContain("side effects");
|
|
206
|
+
expect(op!.description).toContain(
|
|
207
|
+
"Retry semantics: mit demselben Idempotency-Key sicher wiederholbar.",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("formatAgentOpenApi: absent agentGuidance falls back to the base description (RFC-1115 AC-5)", () => {
|
|
212
|
+
const manifest = buildAgentSurfaceManifest({
|
|
213
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
214
|
+
site: "s",
|
|
215
|
+
baseUrl: "https://s.example",
|
|
216
|
+
languages: { default: "de", supported: ["de"] },
|
|
217
|
+
actions: [
|
|
218
|
+
{
|
|
219
|
+
id: "lead.submit",
|
|
220
|
+
url: "/api/agent/actions/lead.submit",
|
|
221
|
+
title: { de: "Anfrage senden" },
|
|
222
|
+
inputSchemaRef: "#/components/schemas/lead.submit-input",
|
|
223
|
+
entitlement: "agent.actions",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
const doc = formatAgentOpenApi(manifest, [
|
|
228
|
+
{
|
|
229
|
+
id: "lead.submit",
|
|
230
|
+
input: { type: "object" },
|
|
231
|
+
output: { type: "object" },
|
|
232
|
+
description: { de: "Reicht eine Kontaktanfrage ein." },
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
const op = doc.paths["/api/agent/actions/lead.submit"]?.post;
|
|
236
|
+
expect(op!.description).toBe("Reicht eine Kontaktanfrage ein.");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("formatAgentOpenApi: search-response schema matches the flattened citation shape (RFC-1115 AC-1)", () => {
|
|
240
|
+
const manifest = buildAgentSurfaceManifest({
|
|
241
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
242
|
+
site: "s",
|
|
243
|
+
baseUrl: "https://s.example",
|
|
244
|
+
languages: { default: "de", supported: ["de"] },
|
|
245
|
+
search: { url: "/api/agent/search", model: "@cf/baai/bge-m3", dimensions: 1024 },
|
|
246
|
+
});
|
|
247
|
+
const doc = formatAgentOpenApi(manifest, []);
|
|
248
|
+
const schema = doc.components.schemas["search-response"] as {
|
|
249
|
+
properties: { results: { items: { required: string[]; properties: Record<string, unknown> } } };
|
|
250
|
+
};
|
|
251
|
+
const item = schema.properties.results.items;
|
|
252
|
+
expect(item.required).toEqual([
|
|
253
|
+
"title",
|
|
254
|
+
"canonicalUrl",
|
|
255
|
+
"updatedAt",
|
|
256
|
+
"lang",
|
|
257
|
+
"type",
|
|
258
|
+
"score",
|
|
259
|
+
"text",
|
|
260
|
+
]);
|
|
261
|
+
expect(item.properties.canonicalUrl).toEqual({ type: "string" });
|
|
262
|
+
expect(item.properties.chunk).toBeUndefined();
|
|
263
|
+
});
|
package/src/component/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>Barrel export for @warpgogol/werkstatt-shared/component sub-path. Re-exports component contract types (ComponentId, CapabilityId, EffectClass, IsolationTier, ComponentDeclaration) sunk from @warpgogol/werkstatt-engine per RFC-1104.</purpose>
|
|
4
|
-
<non-goals>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Does not re-export engine component-runtime modules (identity, schemas, runtime registry).</item>
|
|
6
|
+
</non-goals>
|
|
5
7
|
</MODULE_CONTRACT>
|
|
6
8
|
<CHANGE_SUMMARY>
|
|
7
9
|
<item>RFC-1104: initial barrel — component contracts sunk from werkstatt-engine.</item>
|
package/src/fingerprint/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>Barrel export for @warpgogol/werkstatt-shared/fingerprint sub-path. Re-exports canonical JSON and hashing primitives sunk from @warpgogol/werkstatt-engine per RFC-1104.</purpose>
|
|
4
|
-
<non-goals>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Does not re-export engine fingerprint modules that stay in engine (fingerprint.ts, path-matcher.ts, normalizers).</item>
|
|
6
|
+
</non-goals>
|
|
5
7
|
</MODULE_CONTRACT>
|
|
6
8
|
<CHANGE_SUMMARY>
|
|
7
9
|
<item>RFC-1104: initial barrel — canonical-json + primitives sunk from werkstatt-engine.</item>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1112: durable receipt store for agent-action idempotency, backed by the
|
|
5
|
+
same Upstash Redis EU REST endpoint as the RFC-0181 delivery ledger. Maps an
|
|
6
|
+
Idempotency-Key to { receipt, bodyHash } so a replayed request returns the
|
|
7
|
+
original receipt without re-dispatching, and a key reused with a different
|
|
8
|
+
payload is rejected. Stores identifiers only — never lead payload fields.
|
|
9
|
+
</purpose>
|
|
10
|
+
<non-goals>
|
|
11
|
+
<item>Do not log or embed secrets — the caller injects RestRedisConfig.</item>
|
|
12
|
+
<item>Do not implement the port contract here — the engine's IdempotencyPort
|
|
13
|
+
is structurally identical; this adapter satisfies it.</item>
|
|
14
|
+
</non-goals>
|
|
15
|
+
</MODULE_CONTRACT>
|
|
16
|
+
<CHANGE_SUMMARY>
|
|
17
|
+
<item>RFC-1112: initial receipt store + Upstash Redis REST adapter.</item>
|
|
18
|
+
</CHANGE_SUMMARY>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ActionReceipt } from "@warpgogol/werkstatt-shared/agent";
|
|
22
|
+
import type { RestRedisConfig } from "./qstash.ts";
|
|
23
|
+
|
|
24
|
+
/** Stored record: the original receipt plus the request-body hash it was issued for. */
|
|
25
|
+
export interface ReceiptStoreRecord {
|
|
26
|
+
receipt: ActionReceipt;
|
|
27
|
+
/** sha256 of the raw request body (HTTP) or canonicalJsonHashV1 of arguments (MCP). */
|
|
28
|
+
bodyHash: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Receipt store contract (RFC-1112). Structurally identical to the engine's
|
|
33
|
+
* IdempotencyPort — kept here so the adapter type-checks without importing
|
|
34
|
+
* engine code (werkstatt-shared is a leaf package).
|
|
35
|
+
*/
|
|
36
|
+
export interface ActionReceiptStore {
|
|
37
|
+
get(key: string): Promise<ReceiptStoreRecord | null>;
|
|
38
|
+
put(key: string, bodyHash: string, receipt: ActionReceipt, ttlSeconds: number): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Adapt an Upstash Redis REST endpoint to the ActionReceiptStore (RFC-1112).
|
|
43
|
+
* `get` is a plain GET; `put` is `SET key JSON NX EX ttl` — NX keeps the first
|
|
44
|
+
* stored receipt when two same-key requests race. Throws on transport errors
|
|
45
|
+
* so the caller can decide fail-open semantics.
|
|
46
|
+
*/
|
|
47
|
+
export function restRedisReceiptStore(
|
|
48
|
+
config: RestRedisConfig,
|
|
49
|
+
fetchImpl: typeof fetch = fetch,
|
|
50
|
+
): ActionReceiptStore {
|
|
51
|
+
const prefix = config.prefix ?? "gogol-agent-idem";
|
|
52
|
+
const headers = {
|
|
53
|
+
authorization: `Bearer ${config.token}`,
|
|
54
|
+
"content-type": "application/json",
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
async get(key: string): Promise<ReceiptStoreRecord | null> {
|
|
58
|
+
const response = await fetchImpl(config.url, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers,
|
|
61
|
+
body: JSON.stringify(["GET", `${prefix}:${key}`]),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`redis receipt GET failed: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
const data = (await response.json()) as { result?: string | null };
|
|
67
|
+
if (typeof data.result !== "string") return null;
|
|
68
|
+
return JSON.parse(data.result) as ReceiptStoreRecord;
|
|
69
|
+
},
|
|
70
|
+
async put(
|
|
71
|
+
key: string,
|
|
72
|
+
bodyHash: string,
|
|
73
|
+
receipt: ActionReceipt,
|
|
74
|
+
ttlSeconds: number,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const record: ReceiptStoreRecord = { receipt, bodyHash };
|
|
77
|
+
const command = ["SET", `${prefix}:${key}`, JSON.stringify(record), "NX", "EX", String(ttlSeconds)];
|
|
78
|
+
const response = await fetchImpl(config.url, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers,
|
|
81
|
+
body: JSON.stringify(command),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`redis receipt SET failed: ${response.status}`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/integration/index.ts
CHANGED
|
@@ -116,6 +116,10 @@ export {
|
|
|
116
116
|
restRedisLedger,
|
|
117
117
|
} from "./qstash.ts";
|
|
118
118
|
|
|
119
|
+
// RFC-1112: agent-action receipt store (Upstash Redis EU).
|
|
120
|
+
export type { ReceiptStoreRecord, ActionReceiptStore } from "./idempotency.ts";
|
|
121
|
+
export { restRedisReceiptStore } from "./idempotency.ts";
|
|
122
|
+
|
|
119
123
|
// Runtime modules — explicit re-exports to keep the public API surface clear.
|
|
120
124
|
export type {
|
|
121
125
|
DeliverResult,
|