@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.
Files changed (43) hide show
  1. package/package.json +18 -1
  2. package/src/agent/api-catalog.ts +14 -1
  3. package/src/agent/guidance.ts +52 -0
  4. package/src/agent/health.ts +68 -0
  5. package/src/agent/index.ts +5 -1
  6. package/src/agent/manifest.ts +70 -7
  7. package/src/agent/openapi.ts +97 -4
  8. package/src/agent/receipt.ts +123 -0
  9. package/src/agent/search.test.ts +3 -0
  10. package/src/agent/search.ts +25 -4
  11. package/src/agent/tests/api-catalog.test.ts +6 -0
  12. package/src/agent/tests/ard-catalog.test.ts +9 -0
  13. package/src/agent/tests/manifest.test.ts +71 -0
  14. package/src/agent/tests/mcp-card.test.ts +4 -0
  15. package/src/agent/tests/openapi.test.ts +117 -0
  16. package/src/component/index.ts +3 -1
  17. package/src/fingerprint/index.ts +3 -1
  18. package/src/integration/idempotency.ts +88 -0
  19. package/src/integration/index.ts +4 -0
  20. package/src/integration/tests/idempotency.test.ts +81 -0
  21. package/src/kernel/drift-guard.ts +166 -0
  22. package/src/kernel/index.ts +8 -1
  23. package/src/kernel/tests/drift-guard.test.ts +112 -0
  24. package/src/kernel/tests/workpiece-env.test.ts +44 -0
  25. package/src/kernel/types.ts +63 -4
  26. package/src/kernel/workpiece-env.ts +48 -0
  27. package/src/middleware/access-protection.ts +3 -0
  28. package/src/middleware/language-redirect.ts +3 -0
  29. package/src/observability/index.ts +8 -0
  30. package/src/observability/pipeline-log.ts +81 -0
  31. package/src/ontology/capabilities/lead.prepare.yaml +44 -0
  32. package/src/ontology/capabilities/lead.submit.yaml +20 -3
  33. package/src/ontology/operations/index.ts +6 -0
  34. package/src/ontology/operations/notausgang.ts +44 -0
  35. package/src/ontology/schemas/capability.ts +55 -3
  36. package/src/passport/claim-sign.ts +120 -0
  37. package/src/passport/index.ts +3 -0
  38. package/src/plugin/plugin-contract.ts +1 -1
  39. package/src/semantic/canonical-uri.ts +28 -8
  40. package/src/semantic/llms.ts +10 -2
  41. package/src/semantic/tests/canonical-uri.test.ts +18 -6
  42. package/src/signing/index.ts +3 -1
  43. package/src/stack/run-tool.ts +2 -4
@@ -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 = "1";
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
- /** A single search result — chunk + similarity score. */
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: SearchChunk;
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
+ });
@@ -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>Does not re-export engine component-runtime modules (identity, schemas, runtime registry).</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>
@@ -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>Does not re-export engine fingerprint modules that stay in engine (fingerprint.ts, path-matcher.ts, normalizers).</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
+ }
@@ -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,