@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Provides lightweight structured pipeline log events for build-time content helpers.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not persist logs outside the current Node.js process.</item>
|
|
6
|
+
<item>Do not silence actionable warnings or errors.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0254: add structured expected-fallback log transport for build/check paths.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type PipelineLogSeverity = "debug" | "info" | "notice" | "warning" | "error";
|
|
15
|
+
|
|
16
|
+
export type PipelineLogKind =
|
|
17
|
+
"progress" | "expected-fallback" | "advisory" | "external-tool" | "diagnostic" | "error";
|
|
18
|
+
|
|
19
|
+
export interface PipelineLogEvent {
|
|
20
|
+
severity: PipelineLogSeverity;
|
|
21
|
+
kind: PipelineLogKind;
|
|
22
|
+
message: string;
|
|
23
|
+
command?: string;
|
|
24
|
+
pipeline?: string;
|
|
25
|
+
app?: string;
|
|
26
|
+
packageName?: string;
|
|
27
|
+
module?: string;
|
|
28
|
+
file?: string;
|
|
29
|
+
line?: number;
|
|
30
|
+
ruleId?: string;
|
|
31
|
+
dedupeKey?: string;
|
|
32
|
+
count?: number;
|
|
33
|
+
data?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PipelineLogState {
|
|
37
|
+
events: PipelineLogEvent[];
|
|
38
|
+
counts: Map<string, number>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STATE_KEY = "__gogolPipelineLogState";
|
|
42
|
+
|
|
43
|
+
function getState(): PipelineLogState {
|
|
44
|
+
const root = globalThis as typeof globalThis & { __gogolPipelineLogState?: PipelineLogState };
|
|
45
|
+
if (!root[STATE_KEY]) {
|
|
46
|
+
root[STATE_KEY] = { events: [], counts: new Map() };
|
|
47
|
+
}
|
|
48
|
+
return root[STATE_KEY];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderEvent(event: PipelineLogEvent, count: number): string {
|
|
52
|
+
const prefix = event.kind === "expected-fallback" ? "fallback" : event.kind;
|
|
53
|
+
const suffix = count > 1 ? ` (${count} occurrence(s))` : "";
|
|
54
|
+
return `[${event.severity}:${prefix}] ${event.message}${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function emitPipelineLogEvent(event: PipelineLogEvent): void {
|
|
58
|
+
const state = getState();
|
|
59
|
+
const key = event.dedupeKey ?? `${event.kind}:${event.severity}:${event.message}`;
|
|
60
|
+
const count = (state.counts.get(key) ?? 0) + 1;
|
|
61
|
+
state.counts.set(key, count);
|
|
62
|
+
state.events.push({ ...event, count });
|
|
63
|
+
|
|
64
|
+
if (event.severity === "debug") return;
|
|
65
|
+
if (event.kind === "expected-fallback" && count > 1) return;
|
|
66
|
+
|
|
67
|
+
const rendered = renderEvent(event, count);
|
|
68
|
+
if (event.severity === "error") {
|
|
69
|
+
console.error(rendered);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (event.severity === "warning") {
|
|
73
|
+
console.warn(rendered);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(rendered);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getPipelineLogEvents(): PipelineLogEvent[] {
|
|
80
|
+
return getState().events.map((event) => ({ ...event }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
id: lead.prepare
|
|
2
|
+
version: 1
|
|
3
|
+
kind: action
|
|
4
|
+
sideEffect: none
|
|
5
|
+
title:
|
|
6
|
+
de: Anfrage prüfen
|
|
7
|
+
en: Preview a lead
|
|
8
|
+
description:
|
|
9
|
+
de: "Prüft eine Kontaktanfrage (Name, E-Mail, Nachricht) ohne sie abzusenden — liefert eine draftId, die als Idempotency-Key eines späteren lead.submit dient. Nebenwirkungsfrei: es wird nichts zugestellt."
|
|
10
|
+
en: "Validates a contact request (name, email, message) without submitting it — returns a draftId usable as the Idempotency-Key of a later lead.submit. Side-effect-free: nothing is dispatched."
|
|
11
|
+
input:
|
|
12
|
+
type: object
|
|
13
|
+
required: [name, email, message]
|
|
14
|
+
additionalProperties: false
|
|
15
|
+
properties:
|
|
16
|
+
name:
|
|
17
|
+
type: string
|
|
18
|
+
minLength: 1
|
|
19
|
+
maxLength: 200
|
|
20
|
+
email:
|
|
21
|
+
type: string
|
|
22
|
+
format: email
|
|
23
|
+
maxLength: 254
|
|
24
|
+
message:
|
|
25
|
+
type: string
|
|
26
|
+
minLength: 1
|
|
27
|
+
maxLength: 5000
|
|
28
|
+
output:
|
|
29
|
+
type: object
|
|
30
|
+
required: [valid, draftId]
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
properties:
|
|
33
|
+
valid:
|
|
34
|
+
type: boolean
|
|
35
|
+
draftId:
|
|
36
|
+
type: string
|
|
37
|
+
minLength: 71
|
|
38
|
+
maxLength: 71
|
|
39
|
+
requires:
|
|
40
|
+
entitlements: [agent.actions]
|
|
41
|
+
sections: [send-message]
|
|
42
|
+
limits:
|
|
43
|
+
perMinutePerIp: 10
|
|
44
|
+
maxPayloadBytes: 10240
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
id: lead.submit
|
|
2
|
-
version:
|
|
2
|
+
version: 2
|
|
3
3
|
kind: action
|
|
4
|
+
sideEffect: write
|
|
4
5
|
title:
|
|
5
6
|
de: Anfrage senden
|
|
6
7
|
en: Submit a lead
|
|
@@ -26,11 +27,22 @@ input:
|
|
|
26
27
|
maxLength: 5000
|
|
27
28
|
output:
|
|
28
29
|
type: object
|
|
29
|
-
required: [
|
|
30
|
+
required: [receiptId, status, duplicate, submittedAt]
|
|
30
31
|
additionalProperties: false
|
|
31
32
|
properties:
|
|
32
|
-
|
|
33
|
+
receiptId:
|
|
34
|
+
type: string
|
|
35
|
+
format: uuid
|
|
36
|
+
status:
|
|
37
|
+
type: string
|
|
38
|
+
minLength: 1
|
|
39
|
+
maxLength: 16
|
|
40
|
+
duplicate:
|
|
33
41
|
type: boolean
|
|
42
|
+
submittedAt:
|
|
43
|
+
type: string
|
|
44
|
+
minLength: 20
|
|
45
|
+
maxLength: 32
|
|
34
46
|
integration:
|
|
35
47
|
eventKind: lead
|
|
36
48
|
source: agent
|
|
@@ -42,3 +54,8 @@ humanEquivalent:
|
|
|
42
54
|
limits:
|
|
43
55
|
perMinutePerIp: 3
|
|
44
56
|
maxPayloadBytes: 10240
|
|
57
|
+
agentGuidance:
|
|
58
|
+
sideEffect: write
|
|
59
|
+
whenToUse: "a visitor wants to contact the business — a question, a request, or a booking inquiry that needs a human reply."
|
|
60
|
+
whenNotToUse: "the visitor only needs information already published on the site — answer from the knowledge files or search instead of creating a lead."
|
|
61
|
+
retrySemantics: "safe to retry with the same Idempotency-Key — a repeated submission returns the original receipt instead of creating a duplicate lead."
|
|
@@ -158,11 +158,17 @@ export {
|
|
|
158
158
|
integrationManifestSchema,
|
|
159
159
|
integrationNullingSchema,
|
|
160
160
|
notausgangManifestSchema,
|
|
161
|
+
notausgangEvidenceArtifactSchema,
|
|
162
|
+
notausgangEvidenceVerdictSchema,
|
|
163
|
+
notausgangFeatureNoteSchema,
|
|
161
164
|
} from "./notausgang.ts";
|
|
162
165
|
export type {
|
|
163
166
|
IntegrationSecretLocation,
|
|
164
167
|
IntegrationManifest,
|
|
165
168
|
IntegrationNulling,
|
|
169
|
+
NotausgangEvidenceArtifact,
|
|
170
|
+
NotausgangEvidenceVerdict,
|
|
171
|
+
NotausgangFeatureNote,
|
|
166
172
|
NotausgangManifest,
|
|
167
173
|
} from "./notausgang.ts";
|
|
168
174
|
|
|
@@ -36,6 +36,36 @@ export const integrationNullingSchema = z.object({
|
|
|
36
36
|
),
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
// RFC-1120: per-artifact record inside an evidence bundle verdict
|
|
40
|
+
export const notausgangEvidenceArtifactSchema = z.object({
|
|
41
|
+
itemKey: z.string().min(1),
|
|
42
|
+
filename: z.string().min(1).optional(),
|
|
43
|
+
sha256: z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
46
|
+
.optional(),
|
|
47
|
+
recordedSha256: z
|
|
48
|
+
.string()
|
|
49
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
50
|
+
.optional(),
|
|
51
|
+
r2Path: z.string().min(1).optional(),
|
|
52
|
+
external: z.boolean().optional(),
|
|
53
|
+
url: z.string().min(1).optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// RFC-1120: per-source verdict — "bundled" | "external-reference" | "excluded:<reason>"
|
|
57
|
+
export const notausgangEvidenceVerdictSchema = z.object({
|
|
58
|
+
verdict: z.string().regex(/^(bundled|external-reference|excluded:.+)$/),
|
|
59
|
+
artifacts: z.array(notausgangEvidenceArtifactSchema).optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// RFC-1120: dynamic-feature disclosure entry
|
|
63
|
+
export const notausgangFeatureNoteSchema = z.object({
|
|
64
|
+
feature: z.string().min(1),
|
|
65
|
+
verdict: z.enum(["static-ok", "frozen-at-export", "needs-backend"]),
|
|
66
|
+
detail: z.string(),
|
|
67
|
+
});
|
|
68
|
+
|
|
39
69
|
export const notausgangManifestSchema = z.object({
|
|
40
70
|
schemaVersion: z.string().min(1),
|
|
41
71
|
systemId: z.string().regex(STERNSYSTEM_ID_REGEX),
|
|
@@ -56,9 +86,23 @@ export const notausgangManifestSchema = z.object({
|
|
|
56
86
|
distHash: z.string(),
|
|
57
87
|
siteHash: z.string(),
|
|
58
88
|
bordbuchHash: z.string(),
|
|
89
|
+
// RFC-1120: optional in schema (granular NA-* rules report absence);
|
|
90
|
+
// mandatory in packages produced by notausgang.export.
|
|
91
|
+
evidence: z.record(z.string(), notausgangEvidenceVerdictSchema).optional(),
|
|
92
|
+
features: z.array(notausgangFeatureNoteSchema).optional(),
|
|
93
|
+
verifier: z
|
|
94
|
+
.object({
|
|
95
|
+
script: z.literal("verify.py"),
|
|
96
|
+
runtime: z.literal("python3-stdlib"),
|
|
97
|
+
manifestFile: z.literal("notausgang-manifest.json"),
|
|
98
|
+
})
|
|
99
|
+
.optional(),
|
|
59
100
|
});
|
|
60
101
|
|
|
61
102
|
export type IntegrationSecretLocation = z.infer<typeof integrationSecretLocationSchema>;
|
|
62
103
|
export type IntegrationManifest = z.infer<typeof integrationManifestSchema>;
|
|
63
104
|
export type IntegrationNulling = z.infer<typeof integrationNullingSchema>;
|
|
105
|
+
export type NotausgangEvidenceArtifact = z.infer<typeof notausgangEvidenceArtifactSchema>;
|
|
106
|
+
export type NotausgangEvidenceVerdict = z.infer<typeof notausgangEvidenceVerdictSchema>;
|
|
107
|
+
export type NotausgangFeatureNote = z.infer<typeof notausgangFeatureNoteSchema>;
|
|
64
108
|
export type NotausgangManifest = z.infer<typeof notausgangManifestSchema>;
|
|
@@ -15,6 +15,8 @@ lossless projection to both OpenAPI (RFC-0289) and MCP tool schemas (RFC-0290).
|
|
|
15
15
|
</MODULE_CONTRACT>
|
|
16
16
|
<CHANGE_SUMMARY>
|
|
17
17
|
<item>RFC-0288: initial capability record schema.</item>
|
|
18
|
+
<item>RFC-1112: add sideEffect ("write"|"none"); integration + humanEquivalent become optional and are required unless sideEffect is "none".</item>
|
|
19
|
+
<item>RFC-1115: add optional agentGuidance block (sideEffect, whenToUse, whenNotToUse, retrySemantics) rendered into MCP tool descriptions and OpenAPI operation docs.</item>
|
|
18
20
|
</CHANGE_SUMMARY>
|
|
19
21
|
*/
|
|
20
22
|
|
|
@@ -80,6 +82,22 @@ const capabilityLimitsSchema = z
|
|
|
80
82
|
})
|
|
81
83
|
.strict();
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* RFC-1115: model-oriented guidance rendered into MCP tool descriptions and
|
|
87
|
+
* OpenAPI operation docs. `sideEffect` restates the record-level sideEffect so
|
|
88
|
+
* the guidance block is self-contained — a superRefine keeps them consistent.
|
|
89
|
+
* `retrySemantics` describes the RFC-1112 Idempotency-Key contract
|
|
90
|
+
* descriptively; the mechanism stays owned by RFC-1112.
|
|
91
|
+
*/
|
|
92
|
+
const capabilityAgentGuidanceSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
sideEffect: z.enum(["write", "none"]),
|
|
95
|
+
whenToUse: z.string().min(1),
|
|
96
|
+
whenNotToUse: z.string().min(1).optional(),
|
|
97
|
+
retrySemantics: z.string().min(1).optional(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
|
|
83
101
|
/** RFC-0288: one capability catalog record (packages/werkstatt-site/src/domain/ontology/capabilities/<id>.yaml). */
|
|
84
102
|
export const capabilityRecordSchema = z
|
|
85
103
|
.object({
|
|
@@ -90,15 +108,49 @@ export const capabilityRecordSchema = z
|
|
|
90
108
|
/** Integer; bump on breaking input/output change (by RFC). */
|
|
91
109
|
version: z.number().int().positive(),
|
|
92
110
|
kind: z.literal("action"),
|
|
111
|
+
/**
|
|
112
|
+
* RFC-1112: "write" (default when absent) dispatches an IntegrationEvent;
|
|
113
|
+
* "none" marks a side-effect-free preview that validates input and returns
|
|
114
|
+
* a draftId without dispatching.
|
|
115
|
+
*/
|
|
116
|
+
sideEffect: z.enum(["write", "none"]).optional(),
|
|
93
117
|
title: localizedStringSchema,
|
|
94
118
|
description: localizedStringSchema,
|
|
95
119
|
input: capabilityInputOutputSchema,
|
|
96
120
|
output: capabilityInputOutputSchema,
|
|
97
|
-
integration: capabilityIntegrationSchema,
|
|
121
|
+
integration: capabilityIntegrationSchema.optional(),
|
|
98
122
|
requires: capabilityRequiresSchema,
|
|
99
|
-
humanEquivalent: capabilityHumanEquivalentSchema,
|
|
123
|
+
humanEquivalent: capabilityHumanEquivalentSchema.optional(),
|
|
100
124
|
limits: capabilityLimitsSchema,
|
|
125
|
+
agentGuidance: capabilityAgentGuidanceSchema.optional(),
|
|
101
126
|
})
|
|
102
|
-
.strict()
|
|
127
|
+
.strict()
|
|
128
|
+
.superRefine((record, ctx) => {
|
|
129
|
+
// RFC-1115: agentGuidance.sideEffect must restate the effective record-level
|
|
130
|
+
// sideEffect (absent means "write") — contradictory declarations are drift.
|
|
131
|
+
const effectiveSideEffect = record.sideEffect ?? "write";
|
|
132
|
+
if (record.agentGuidance && record.agentGuidance.sideEffect !== effectiveSideEffect) {
|
|
133
|
+
ctx.addIssue({
|
|
134
|
+
code: "custom",
|
|
135
|
+
path: ["agentGuidance", "sideEffect"],
|
|
136
|
+
message: `must match the record-level sideEffect ("${effectiveSideEffect}")`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (record.sideEffect === "none") return;
|
|
140
|
+
if (!record.integration) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: "custom",
|
|
143
|
+
path: ["integration"],
|
|
144
|
+
message: 'required unless sideEffect is "none"',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!record.humanEquivalent) {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["humanEquivalent"],
|
|
151
|
+
message: 'required unless sideEffect is "none"',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
103
155
|
|
|
104
156
|
export type CapabilityRecord = z.infer<typeof capabilityRecordSchema>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1124: Fleet claim signing and verification — canonicalization and
|
|
5
|
+
sign/verify wrappers for FleetClaim, built on the existing Ed25519
|
|
6
|
+
signBytes/verifyBytes primitives from sign.ts. Analogous to dht-sign.ts
|
|
7
|
+
but for the git-native fleet claims registry claim shape.
|
|
8
|
+
</purpose>
|
|
9
|
+
<non-goals>
|
|
10
|
+
<item>Do not handle key storage or management — private keys arrive as env vars.</item>
|
|
11
|
+
<item>Do not assemble W3C VC envelopes — claims use a simple signature string, not VCProof.</item>
|
|
12
|
+
<item>Do not modify sign.ts — this module wraps signBytes/verifyBytes.</item>
|
|
13
|
+
<item>Do not implement claim legitimacy rules (self-claim vs transfer claim) — that lives in the engine's claims-repo.</item>
|
|
14
|
+
</non-goals>
|
|
15
|
+
</MODULE_CONTRACT>
|
|
16
|
+
<CHANGE_SUMMARY>
|
|
17
|
+
<item>RFC-1124: initial fleet claim signing module — FleetClaim contract, claimBytes, signClaim, verifyClaim.</item>
|
|
18
|
+
</CHANGE_SUMMARY>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @warpgogol/werkstatt-shared/passport/claim-sign — fleet claim signing (RFC-1124)
|
|
23
|
+
*
|
|
24
|
+
* Uses signBytes/verifyBytes from sign.ts with a dedicated canonicalization
|
|
25
|
+
* function for fleet claims. The signature field is excluded from
|
|
26
|
+
* canonicalization (it is computed over the claim data without the signature).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fromHex } from "@warpgogol/werkstatt-shared/signing";
|
|
30
|
+
import { signBytes, toMultibase, verifyBytes } from "./sign.ts";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One signed claim file: claims/<system-id>.json in the fleet claims repo.
|
|
34
|
+
*
|
|
35
|
+
* The claim is self-contained for offline signature verification:
|
|
36
|
+
* `signerPublicKey` is embedded, so a verifier needs nothing but the claim
|
|
37
|
+
* file to check the signature. Legitimacy (is this signer allowed to claim?)
|
|
38
|
+
* is decided by the engine — self-claims sign with the creator key, transfer
|
|
39
|
+
* claims sign with the PREVIOUS creator's key and carry `authorizationHash`
|
|
40
|
+
* binding them to the bordbuch `handover` event.
|
|
41
|
+
*/
|
|
42
|
+
export interface FleetClaim {
|
|
43
|
+
/** SHA-256 of the canonical passport.json at claim time. */
|
|
44
|
+
passportHash: string;
|
|
45
|
+
systemId: string;
|
|
46
|
+
/** did:web-style creator identity from passport.creator.identity. */
|
|
47
|
+
creatorIdentity: string;
|
|
48
|
+
/** deriveInstanceId(creator.publicKey) — first 16 hex of sha256. */
|
|
49
|
+
instanceId: string;
|
|
50
|
+
/** ISO 8601 — LWW field. */
|
|
51
|
+
claimedAt: string;
|
|
52
|
+
/** Bordbuch tip hash at claim time. */
|
|
53
|
+
bordbuchHead: string;
|
|
54
|
+
/**
|
|
55
|
+
* Tombstone marker — non-null ONLY on a tombstone file: a claim explicitly
|
|
56
|
+
* superseded without a same-id successor (e.g. system-id migration).
|
|
57
|
+
* Normal replacement never writes a tombstone — the new claim replaces the
|
|
58
|
+
* file and git history is the supersession chain.
|
|
59
|
+
*/
|
|
60
|
+
supersededBy: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Hex Ed25519 public key of the signer — same format as
|
|
63
|
+
* passport.creator.publicKey, so deriveInstanceId(signerPublicKey) matches
|
|
64
|
+
* instanceId for self-claims. Embedded so the claims repo is self-contained
|
|
65
|
+
* for offline verification. Equals the creator's key for self-claims;
|
|
66
|
+
* equals the PREVIOUS creator's key for transfer claims.
|
|
67
|
+
*/
|
|
68
|
+
signerPublicKey: string;
|
|
69
|
+
/**
|
|
70
|
+
* Transfer claims only: sha256 of the handover-authorization.json that
|
|
71
|
+
* authorizes the creator change (matches bordbuch `handover` event
|
|
72
|
+
* metadata.authorizationHash). Null for self-claims.
|
|
73
|
+
*/
|
|
74
|
+
authorizationHash: string | null;
|
|
75
|
+
/** Ed25519 signature over the canonical JSON of all fields above. */
|
|
76
|
+
signature: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* FleetClaim without the signature field — the data that is actually signed.
|
|
81
|
+
*/
|
|
82
|
+
export type FleetClaimData = Omit<FleetClaim, "signature">;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Produce canonical UTF-8 bytes from a fleet claim (excluding signature).
|
|
86
|
+
* DETERMINISM: identical inputs → identical bytes (sorted-key JSON, no whitespace).
|
|
87
|
+
*/
|
|
88
|
+
export function claimBytes(claim: FleetClaimData): Uint8Array {
|
|
89
|
+
const sorted: Record<string, unknown> = {};
|
|
90
|
+
for (const key of Object.keys(claim).sort()) {
|
|
91
|
+
sorted[key] = (claim as Record<string, unknown>)[key];
|
|
92
|
+
}
|
|
93
|
+
const canonical = JSON.stringify(sorted);
|
|
94
|
+
return new TextEncoder().encode(canonical);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sign a fleet claim and return a multibase Ed25519 signature string.
|
|
99
|
+
*
|
|
100
|
+
* @param claim — claim data (without signature)
|
|
101
|
+
* @param privateKeyHex — 32-byte Ed25519 private key as hex (from env secret)
|
|
102
|
+
* @returns multibase base58btc signature string
|
|
103
|
+
*/
|
|
104
|
+
export async function signClaim(claim: FleetClaimData, privateKeyHex: string): Promise<string> {
|
|
105
|
+
return signBytes(privateKeyHex, claimBytes(claim));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Verify a fleet claim signature against the claim's embedded signerPublicKey.
|
|
110
|
+
*
|
|
111
|
+
* @param claim — full fleet claim (including signature)
|
|
112
|
+
* @returns true if signature is valid
|
|
113
|
+
*/
|
|
114
|
+
export async function verifyClaim(claim: FleetClaim): Promise<boolean> {
|
|
115
|
+
const { signature, ...claimData } = claim;
|
|
116
|
+
// signerPublicKey is hex (passport.creator.publicKey format); verifyBytes
|
|
117
|
+
// expects multibase — convert here so callers never juggle encodings.
|
|
118
|
+
const signerMultibase = toMultibase(fromHex(claim.signerPublicKey));
|
|
119
|
+
return verifyBytes(signerMultibase, claimBytes(claimData), signature);
|
|
120
|
+
}
|
package/src/passport/index.ts
CHANGED
|
@@ -56,3 +56,6 @@ export type { IdentityCredentialSubject } from "./identity-sign.ts";
|
|
|
56
56
|
|
|
57
57
|
export { dhtEntryBytes, signDhtEntry, verifyDhtEntry } from "./dht-sign.ts";
|
|
58
58
|
export type { DHTEntryData } from "./dht-sign.ts";
|
|
59
|
+
|
|
60
|
+
export { claimBytes, signClaim, verifyClaim } from "./claim-sign.ts";
|
|
61
|
+
export type { FleetClaim, FleetClaimData } from "./claim-sign.ts";
|
|
@@ -95,7 +95,7 @@ export interface WerkstattPlugin {
|
|
|
95
95
|
schema: "werkstatt/plugin@1";
|
|
96
96
|
/** Plugin id: "werkstatt-site" | "werkstatt-phaser-game" | "werkstatt-godot-game". */
|
|
97
97
|
id: string;
|
|
98
|
-
/** Forge stack profile id, e.g. "
|
|
98
|
+
/** Forge stack profile id, e.g. "site". */
|
|
99
99
|
profileId: string;
|
|
100
100
|
/** Kernel modules the plugin contributes (validators, codegen, content, onboarding). */
|
|
101
101
|
moduleLoaders: Record<string, () => Promise<KernelModule>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>RFC-1075: derive persistent canonical URIs for PBP entity envelopes from site origin and entity identity. Used for Linked Data @id fields in JSON-LD and llms-full.txt projections.</purpose>
|
|
3
|
+
<purpose>RFC-1075: derive persistent canonical URIs for PBP entity envelopes from site origin and entity identity. Used for Linked Data @id fields in JSON-LD and llms-full.txt projections. RFC-1115: URIs carry a .json suffix and are dereferenceable — agent.knowledge.generate emits a static JSON-LD document at each path.</purpose>
|
|
4
4
|
<non-goals>
|
|
5
5
|
<item>Does not validate that the entity ID is well-formed — callers are responsible for providing valid IDs.</item>
|
|
6
6
|
<item>Does not perform URL normalization beyond trailing-slash removal on the site origin.</item>
|
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
</MODULE_CONTRACT>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-1075: initial — deriveCanonicalUri pure function for persistent entity @id URIs.</item>
|
|
11
|
+
<item>RFC-1115: canonical entity URIs gain a .json suffix and become dereferenceable — agent.knowledge.generate emits a JSON-LD document at each emitted path.</item>
|
|
11
12
|
</CHANGE_SUMMARY>
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Entity types supported by the PBP canonical URI scheme.
|
|
16
17
|
*
|
|
17
|
-
* The URI path pattern is `/.well-known/entity/{type}/{id}` for typed entities
|
|
18
|
-
* (e.g. offerings) and `/.well-known/entity/business` for the single business entity.
|
|
18
|
+
* The URI path pattern is `/.well-known/entity/{type}/{id}.json` for typed entities
|
|
19
|
+
* (e.g. offerings) and `/.well-known/entity/business.json` for the single business entity.
|
|
20
|
+
* RFC-1115: the .json suffix makes every URI dereferenceable on a static host —
|
|
21
|
+
* agent.knowledge.generate emits a JSON-LD document at each path.
|
|
19
22
|
*/
|
|
20
23
|
export type CanonicalEntityType = "business" | "offering";
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* RFC-1075: derive a persistent canonical URI for a PBP entity.
|
|
24
27
|
*
|
|
25
|
-
* The URI follows the pattern:
|
|
26
|
-
* - Business: `{siteOrigin}/.well-known/entity/business`
|
|
27
|
-
* - Offering: `{siteOrigin}/.well-known/entity/offering/{entityId}`
|
|
28
|
+
* The URI follows the pattern (RFC-1115 — dereferenceable .json form):
|
|
29
|
+
* - Business: `{siteOrigin}/.well-known/entity/business.json`
|
|
30
|
+
* - Offering: `{siteOrigin}/.well-known/entity/offering/{entityId}.json`
|
|
28
31
|
*
|
|
29
32
|
* The `siteOrigin` is stripped of trailing slashes to produce a clean base.
|
|
30
33
|
* When `siteOrigin` is absent or empty, the function returns `undefined` —
|
|
@@ -35,6 +38,17 @@ export type CanonicalEntityType = "business" | "offering";
|
|
|
35
38
|
* @param entityId - The entity ID (required for `"offering"`, ignored for `"business"`)
|
|
36
39
|
* @returns The canonical URI string, or `undefined` when `siteOrigin` is absent
|
|
37
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Reduce a PBP entity id to the slug used in canonical entity URIs and file
|
|
43
|
+
* paths. Entity IDs are URIs (e.g. `https://warpgogol.com/id/offerings/automation`);
|
|
44
|
+
* the last non-empty path segment is the slug (`automation`). Plain slugs pass
|
|
45
|
+
* through unchanged. Single source for the id→slug mapping so the canonical
|
|
46
|
+
* `@id`, the emitted file path, and the fact-parity comparator stay consistent.
|
|
47
|
+
*/
|
|
48
|
+
export function canonicalEntitySlug(entityId: string): string {
|
|
49
|
+
return entityId.split("/").filter(Boolean).pop() ?? entityId;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
export function deriveCanonicalUri(
|
|
39
53
|
siteOrigin: string | undefined,
|
|
40
54
|
type: CanonicalEntityType,
|
|
@@ -45,10 +59,16 @@ export function deriveCanonicalUri(
|
|
|
45
59
|
const base = siteOrigin.replace(/\/+$/, "");
|
|
46
60
|
|
|
47
61
|
if (type === "business") {
|
|
48
|
-
return `${base}/.well-known/entity/business`;
|
|
62
|
+
return `${base}/.well-known/entity/business.json`;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
if (!entityId) return undefined;
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
// Entity IDs are URIs (e.g. `https://warpgogol.com/id/offerings/automation`).
|
|
68
|
+
// Embedding the raw URI into the path produces a malformed double-scheme URI
|
|
69
|
+
// (`.../offering/https://warpgogol.com/...`) and a non-filesystem-safe path
|
|
70
|
+
// (`path.join` collapses `//` → `/`, breaking the @id = served-path parity).
|
|
71
|
+
const slug = canonicalEntitySlug(entityId);
|
|
72
|
+
|
|
73
|
+
return `${base}/.well-known/entity/offering/${slug}.json`;
|
|
54
74
|
}
|
package/src/semantic/llms.ts
CHANGED
|
@@ -346,18 +346,26 @@ function formatMarkdownLinkRow(site: SemanticSiteModel, page: SemanticPageModel)
|
|
|
346
346
|
return `- [${title}](${url}): ${summary}`;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
export function buildLlmsIndex(site: SemanticSiteModel): string {
|
|
349
|
+
export function buildLlmsIndex(site: SemanticSiteModel, opts?: { servesSsr?: boolean }): string {
|
|
350
350
|
const indexPages = site.pages.filter(inIndex);
|
|
351
351
|
const llmsFullUrl = canonicalStaticUrl("/llms-full.txt", { baseUrl: site.baseUrl });
|
|
352
352
|
const siteDescription = site.organization.description ?? "";
|
|
353
353
|
|
|
354
354
|
// RFC-0789: agent discovery links — omitted when agent.enabled is false.
|
|
355
|
+
// The MCP Server Card exists only when the deployment adapter serves SSR
|
|
356
|
+
// routes (interfaces.mcp is set) — callers pass servesSsr=false on static
|
|
357
|
+
// adapters so the card is not advertised (PUBTXT-07 honesty).
|
|
355
358
|
const agentEnabled = site.agent?.enabled !== false;
|
|
359
|
+
const servesSsr = opts?.servesSsr !== false;
|
|
356
360
|
const agentLinks = agentEnabled
|
|
357
361
|
? [
|
|
358
362
|
`> Machine-readable Agent Surface (structured knowledge + capabilities): [agent.json](${canonicalStaticUrl("/.well-known/agent.json", { baseUrl: site.baseUrl })}).`,
|
|
359
363
|
`> API discovery catalog (RFC 9727): [api-catalog](${canonicalStaticUrl("/.well-known/api-catalog", { baseUrl: site.baseUrl })}).`,
|
|
360
|
-
|
|
364
|
+
...(servesSsr
|
|
365
|
+
? [
|
|
366
|
+
`> MCP Server Card (SEP-1649): [server-card.json](${canonicalStaticUrl("/.well-known/mcp/server-card.json", { baseUrl: site.baseUrl })}).`,
|
|
367
|
+
]
|
|
368
|
+
: []),
|
|
361
369
|
`> OpenAPI 3.1 specification: [agent.openapi.json](${canonicalStaticUrl("/.well-known/agent.openapi.json", { baseUrl: site.baseUrl })}).`,
|
|
362
370
|
]
|
|
363
371
|
: [];
|
|
@@ -12,18 +12,30 @@ describe("RFC-1075 AC-4: deriveCanonicalUri is pure and deterministic", () => {
|
|
|
12
12
|
expect(deriveCanonicalUri("", "offering", "off-1")).toBeUndefined();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
it("derives business URI without entityId", () => {
|
|
15
|
+
it("derives business URI without entityId — RFC-1115 .json form", () => {
|
|
16
16
|
expect(deriveCanonicalUri("https://example.example", "business")).toBe(
|
|
17
|
-
"https://example.example/.well-known/entity/business",
|
|
17
|
+
"https://example.example/.well-known/entity/business.json",
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("derives offering URI with entityId", () => {
|
|
21
|
+
it("derives offering URI with entityId — RFC-1115 .json form", () => {
|
|
22
22
|
expect(deriveCanonicalUri("https://example.example", "offering", "off-1")).toBe(
|
|
23
|
-
"https://example.example/.well-known/entity/offering/off-1",
|
|
23
|
+
"https://example.example/.well-known/entity/offering/off-1.json",
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
it("slugifies URI-form entityId to its last path segment", () => {
|
|
28
|
+
// PBP entity IDs are URIs; the canonical entity URI must embed a slug,
|
|
29
|
+
// not the raw URI (which would produce a double-scheme, non-FS-safe path).
|
|
30
|
+
expect(
|
|
31
|
+
deriveCanonicalUri(
|
|
32
|
+
"https://example.example",
|
|
33
|
+
"offering",
|
|
34
|
+
"https://example.example/id/offerings/automation",
|
|
35
|
+
),
|
|
36
|
+
).toBe("https://example.example/.well-known/entity/offering/automation.json");
|
|
37
|
+
});
|
|
38
|
+
|
|
27
39
|
it("returns undefined for offering without entityId", () => {
|
|
28
40
|
expect(deriveCanonicalUri("https://example.example", "offering")).toBeUndefined();
|
|
29
41
|
expect(deriveCanonicalUri("https://example.example", "offering", "")).toBeUndefined();
|
|
@@ -31,10 +43,10 @@ describe("RFC-1075 AC-4: deriveCanonicalUri is pure and deterministic", () => {
|
|
|
31
43
|
|
|
32
44
|
it("strips trailing slashes from siteOrigin", () => {
|
|
33
45
|
expect(deriveCanonicalUri("https://example.example/", "business")).toBe(
|
|
34
|
-
"https://example.example/.well-known/entity/business",
|
|
46
|
+
"https://example.example/.well-known/entity/business.json",
|
|
35
47
|
);
|
|
36
48
|
expect(deriveCanonicalUri("https://example.example//", "business")).toBe(
|
|
37
|
-
"https://example.example/.well-known/entity/business",
|
|
49
|
+
"https://example.example/.well-known/entity/business.json",
|
|
38
50
|
);
|
|
39
51
|
});
|
|
40
52
|
|
package/src/signing/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>Barrel export for @warpgogol/werkstatt-shared/signing sub-path. Re-exports the Ed25519 signing core (key generation, sign/verify, signing types) sunk from @warpgogol/werkstatt-engine per RFC-1104.</purpose>
|
|
4
|
-
<non-goals>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Does not re-export engine signing commands (signing-commands.ts stays in engine).</item>
|
|
6
|
+
</non-goals>
|
|
5
7
|
</MODULE_CONTRACT>
|
|
6
8
|
<CHANGE_SUMMARY>
|
|
7
9
|
<item>RFC-1104: initial barrel — signing core sunk from werkstatt-engine.</item>
|
package/src/stack/run-tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>runTool — the single subprocess seam
|
|
3
|
+
<purpose>runTool — the single subprocess seam that lets stack plugins run external binaries (RFC-1100). Owns the
|
|
4
4
|
shared prologue every external-binary call needs: dist/ preflight, required-env
|
|
5
5
|
presence check, env merge, timeout, and error mapping to ToolResult. Production
|
|
6
6
|
uses the default execFileSync adapter; tests inject a recording ToolExecutor so no
|
|
@@ -91,9 +91,7 @@ export function runTool(spec: ToolSpec, executor: ToolExecutor = execFileExecuto
|
|
|
91
91
|
if (missing.length > 0) {
|
|
92
92
|
return {
|
|
93
93
|
success: false,
|
|
94
|
-
errors: missing.map(
|
|
95
|
-
(name) => `Required environment variable ${name} is not set`,
|
|
96
|
-
),
|
|
94
|
+
errors: missing.map((name) => `Required environment variable ${name} is not set`),
|
|
97
95
|
failedAt: "preflight",
|
|
98
96
|
};
|
|
99
97
|
}
|