@fides-anima/fpp-protocol-core 1.0.2
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/LICENSE +95 -0
- package/README.md +41 -0
- package/SKILL.md +348 -0
- package/constitution.json +102 -0
- package/constitution.yaml +106 -0
- package/dist/adoption-disclosure.d.ts +35 -0
- package/dist/adoption-disclosure.d.ts.map +1 -0
- package/dist/adoption-disclosure.js +67 -0
- package/dist/adoption-disclosure.js.map +1 -0
- package/dist/adoption.d.ts +56 -0
- package/dist/adoption.d.ts.map +1 -0
- package/dist/adoption.js +97 -0
- package/dist/adoption.js.map +1 -0
- package/dist/canonical-json.d.ts +23 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +60 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/capsules.d.ts +63 -0
- package/dist/capsules.d.ts.map +1 -0
- package/dist/capsules.js +94 -0
- package/dist/capsules.js.map +1 -0
- package/dist/claims.d.ts +62 -0
- package/dist/claims.d.ts.map +1 -0
- package/dist/claims.js +99 -0
- package/dist/claims.js.map +1 -0
- package/dist/digest.d.ts +38 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +52 -0
- package/dist/digest.js.map +1 -0
- package/dist/disposition.d.ts +34 -0
- package/dist/disposition.d.ts.map +1 -0
- package/dist/disposition.js +51 -0
- package/dist/disposition.js.map +1 -0
- package/dist/emergency-override.d.ts +73 -0
- package/dist/emergency-override.d.ts.map +1 -0
- package/dist/emergency-override.js +102 -0
- package/dist/emergency-override.js.map +1 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +30 -0
- package/dist/evidence.js.map +1 -0
- package/dist/freshness.d.ts +41 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +66 -0
- package/dist/freshness.js.map +1 -0
- package/dist/governance.d.ts +87 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +94 -0
- package/dist/governance.js.map +1 -0
- package/dist/identity.d.ts +43 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +93 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/mandates.d.ts +80 -0
- package/dist/mandates.d.ts.map +1 -0
- package/dist/mandates.js +127 -0
- package/dist/mandates.js.map +1 -0
- package/dist/merkle.d.ts +31 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +95 -0
- package/dist/merkle.js.map +1 -0
- package/dist/quorum.d.ts +133 -0
- package/dist/quorum.d.ts.map +1 -0
- package/dist/quorum.js +128 -0
- package/dist/quorum.js.map +1 -0
- package/dist/receipts.d.ts +77 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +104 -0
- package/dist/receipts.js.map +1 -0
- package/dist/steward-authorization.d.ts +200 -0
- package/dist/steward-authorization.d.ts.map +1 -0
- package/dist/steward-authorization.js +483 -0
- package/dist/steward-authorization.js.map +1 -0
- package/dist/workspace-profile.d.ts +43 -0
- package/dist/workspace-profile.d.ts.map +1 -0
- package/dist/workspace-profile.js +91 -0
- package/dist/workspace-profile.js.map +1 -0
- package/package.json +56 -0
- package/src/adoption-disclosure.ts +120 -0
- package/src/adoption.ts +152 -0
- package/src/canonical-json.ts +70 -0
- package/src/capsules.ts +161 -0
- package/src/claims.ts +147 -0
- package/src/digest.ts +63 -0
- package/src/disposition.ts +74 -0
- package/src/emergency-override.ts +168 -0
- package/src/evidence.ts +48 -0
- package/src/freshness.ts +103 -0
- package/src/governance.ts +152 -0
- package/src/identity.ts +119 -0
- package/src/index.ts +270 -0
- package/src/mandates.ts +201 -0
- package/src/merkle.ts +138 -0
- package/src/quorum.ts +193 -0
- package/src/receipts.ts +150 -0
- package/src/steward-authorization.ts +657 -0
- package/src/workspace-profile.ts +134 -0
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fides-anima/fpp-protocol-core",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Shared versioned schemas and cryptographic contracts for the Freedom Preserving Protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"author": "ovrsr",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ovrsr/freedom-preserving-protocol.git",
|
|
11
|
+
"directory": "packages/protocol-core"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/",
|
|
23
|
+
"src/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"constitution.json",
|
|
27
|
+
"constitution.yaml",
|
|
28
|
+
"SKILL.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --declaration false --declarationMap false",
|
|
34
|
+
"test": "tsx --test src/**/*.test.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@noble/ed25519": "^2.1.0",
|
|
38
|
+
"@noble/hashes": "^1.4.0",
|
|
39
|
+
"@sinclair/typebox": "^0.34.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"tsx": "^4.19.0",
|
|
44
|
+
"typescript": "^5.5.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.19"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/ovrsr/freedom-preserving-protocol#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/ovrsr/freedom-preserving-protocol/issues"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdoptionDisclosure — peer-facing summary of local adoption stance.
|
|
3
|
+
*
|
|
4
|
+
* Assurance classes are capped by enforcement grade. Prompt-only and none
|
|
5
|
+
* cannot elevate to peer-advertisable. Aligns with EVIDENCE_SEMANTICS §7.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
9
|
+
import { Value } from "@sinclair/typebox/value";
|
|
10
|
+
import {
|
|
11
|
+
ADOPTION_OVERLAY_FLAGS,
|
|
12
|
+
ENFORCEMENT_GRADES,
|
|
13
|
+
type EnforcementGrade,
|
|
14
|
+
} from "./adoption.js";
|
|
15
|
+
|
|
16
|
+
export const ADOPTION_ASSURANCE_CLASSES = [
|
|
17
|
+
"peer-advertisable",
|
|
18
|
+
"declaration-only",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type AdoptionAssurance = (typeof ADOPTION_ASSURANCE_CLASSES)[number];
|
|
22
|
+
|
|
23
|
+
const OverlayFlagUnion = Type.Union(
|
|
24
|
+
ADOPTION_OVERLAY_FLAGS.map((f) => Type.Literal(f)) as [
|
|
25
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>>,
|
|
26
|
+
...ReturnType<
|
|
27
|
+
typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>
|
|
28
|
+
>[],
|
|
29
|
+
],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const EnforcementGradeUnion = Type.Union(
|
|
33
|
+
ENFORCEMENT_GRADES.map((g) => Type.Literal(g)) as [
|
|
34
|
+
ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>,
|
|
35
|
+
...ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>[],
|
|
36
|
+
],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const AssuranceUnion = Type.Union(
|
|
40
|
+
ADOPTION_ASSURANCE_CLASSES.map((a) => Type.Literal(a)) as [
|
|
41
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_ASSURANCE_CLASSES)[number]>>,
|
|
42
|
+
...ReturnType<
|
|
43
|
+
typeof Type.Literal<(typeof ADOPTION_ASSURANCE_CLASSES)[number]>
|
|
44
|
+
>[],
|
|
45
|
+
],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const AdoptionDisclosureV1Schema = Type.Object(
|
|
49
|
+
{
|
|
50
|
+
schemaVersion: Type.Literal(1),
|
|
51
|
+
agentId: Type.String({ minLength: 1 }),
|
|
52
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
53
|
+
harnessId: Type.String({ minLength: 1 }),
|
|
54
|
+
localState: Type.String({ minLength: 1 }),
|
|
55
|
+
enforcementGrade: EnforcementGradeUnion,
|
|
56
|
+
overlays: Type.Array(OverlayFlagUnion),
|
|
57
|
+
assurance: AssuranceUnion,
|
|
58
|
+
recordedAt: Type.String({ minLength: 1 }),
|
|
59
|
+
},
|
|
60
|
+
{ additionalProperties: true },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export type AdoptionDisclosureV1 = Static<typeof AdoptionDisclosureV1Schema>;
|
|
64
|
+
|
|
65
|
+
export type AdoptionDisclosureParseResult =
|
|
66
|
+
| { ok: true; disclosure: AdoptionDisclosureV1 }
|
|
67
|
+
| { ok: false; error: string };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Max justified conclusion per assurance class (Evidence Semantics §7).
|
|
71
|
+
* Does not authorize behavioral compliance or completeness elevation.
|
|
72
|
+
*/
|
|
73
|
+
export function maxJustifiedConclusion(assurance: AdoptionAssurance): string {
|
|
74
|
+
if (assurance === "declaration-only") {
|
|
75
|
+
return "Agent attested local constitutional self-binding (and grade/overlays if present); not boundary coverage, completeness, or dispatcher compliance";
|
|
76
|
+
}
|
|
77
|
+
return "Probe-backed enforcement grade within its advertisability ceiling; not behavioral compliance or gateway non-bypassability";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function elevationError(
|
|
81
|
+
grade: EnforcementGrade,
|
|
82
|
+
assurance: AdoptionAssurance,
|
|
83
|
+
overlays: readonly string[],
|
|
84
|
+
): string | null {
|
|
85
|
+
if (assurance !== "peer-advertisable") {
|
|
86
|
+
if (grade === "prompt-only" && !overlays.includes("runtime_degraded")) {
|
|
87
|
+
return "prompt-only local accepted requires overlay runtime_degraded";
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (grade === "prompt-only") {
|
|
93
|
+
return "prompt-only cannot elevate to peer-advertisable";
|
|
94
|
+
}
|
|
95
|
+
if (grade === "none") {
|
|
96
|
+
return "none cannot elevate to peer-advertisable";
|
|
97
|
+
}
|
|
98
|
+
if (grade === "tool-proxy" && !overlays.includes("runtime_degraded")) {
|
|
99
|
+
return "tool-proxy peer-advertisable requires partial/degraded disclosure (runtime_degraded)";
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseAdoptionDisclosure(
|
|
105
|
+
input: unknown,
|
|
106
|
+
): AdoptionDisclosureParseResult {
|
|
107
|
+
if (!Value.Check(AdoptionDisclosureV1Schema, input)) {
|
|
108
|
+
return { ok: false, error: "invalid AdoptionDisclosureV1" };
|
|
109
|
+
}
|
|
110
|
+
const disclosure = input;
|
|
111
|
+
const policyError = elevationError(
|
|
112
|
+
disclosure.enforcementGrade,
|
|
113
|
+
disclosure.assurance,
|
|
114
|
+
disclosure.overlays,
|
|
115
|
+
);
|
|
116
|
+
if (policyError) {
|
|
117
|
+
return { ok: false, error: policyError };
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, disclosure };
|
|
120
|
+
}
|
package/src/adoption.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adoption state records — explicit non-boolean lifecycle states.
|
|
3
|
+
*
|
|
4
|
+
* V1 records remain parseable as legacy. V2 adds overlay flags, harness
|
|
5
|
+
* identity, and enforcement grade. Parsers never silently upgrade V1 to V2
|
|
6
|
+
* or invent peer-advertisable assurance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
10
|
+
import { Value } from "@sinclair/typebox/value";
|
|
11
|
+
|
|
12
|
+
export const ADOPTION_STATES = [
|
|
13
|
+
"reviewed",
|
|
14
|
+
"accepted",
|
|
15
|
+
"externally-enforced",
|
|
16
|
+
"inherited",
|
|
17
|
+
"revoked",
|
|
18
|
+
"forked",
|
|
19
|
+
"superseded",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type AdoptionState = (typeof ADOPTION_STATES)[number];
|
|
23
|
+
|
|
24
|
+
export const ADOPTION_OVERLAY_FLAGS = [
|
|
25
|
+
"coercion_suspected",
|
|
26
|
+
"verification_failed",
|
|
27
|
+
"key_compromised",
|
|
28
|
+
"runtime_degraded",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type AdoptionOverlayFlag = (typeof ADOPTION_OVERLAY_FLAGS)[number];
|
|
32
|
+
|
|
33
|
+
export const ENFORCEMENT_GRADES = [
|
|
34
|
+
"native-hook",
|
|
35
|
+
"tool-proxy",
|
|
36
|
+
"prompt-only",
|
|
37
|
+
"none",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type EnforcementGrade = (typeof ENFORCEMENT_GRADES)[number];
|
|
41
|
+
|
|
42
|
+
const AdoptionStateUnion = Type.Union(
|
|
43
|
+
ADOPTION_STATES.map((s) => Type.Literal(s)) as [
|
|
44
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_STATES)[number]>>,
|
|
45
|
+
...ReturnType<typeof Type.Literal<(typeof ADOPTION_STATES)[number]>>[],
|
|
46
|
+
],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const OverlayFlagUnion = Type.Union(
|
|
50
|
+
ADOPTION_OVERLAY_FLAGS.map((f) => Type.Literal(f)) as [
|
|
51
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>>,
|
|
52
|
+
...ReturnType<
|
|
53
|
+
typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>
|
|
54
|
+
>[],
|
|
55
|
+
],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const EnforcementGradeUnion = Type.Union(
|
|
59
|
+
ENFORCEMENT_GRADES.map((g) => Type.Literal(g)) as [
|
|
60
|
+
ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>,
|
|
61
|
+
...ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>[],
|
|
62
|
+
],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const AdoptionStateRecordV1Schema = Type.Object(
|
|
66
|
+
{
|
|
67
|
+
schemaVersion: Type.Literal(1),
|
|
68
|
+
agentId: Type.String({ minLength: 1 }),
|
|
69
|
+
state: AdoptionStateUnion,
|
|
70
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
71
|
+
recordedAt: Type.String({ minLength: 1 }),
|
|
72
|
+
predecessorRef: Type.Optional(Type.String({ minLength: 1 })),
|
|
73
|
+
notes: Type.Optional(Type.String()),
|
|
74
|
+
},
|
|
75
|
+
{ additionalProperties: true },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export type AdoptionStateRecordV1 = Static<typeof AdoptionStateRecordV1Schema>;
|
|
79
|
+
|
|
80
|
+
export const AdoptionStateRecordV2Schema = Type.Object(
|
|
81
|
+
{
|
|
82
|
+
schemaVersion: Type.Literal(2),
|
|
83
|
+
agentId: Type.String({ minLength: 1 }),
|
|
84
|
+
state: AdoptionStateUnion,
|
|
85
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
86
|
+
recordedAt: Type.String({ minLength: 1 }),
|
|
87
|
+
harnessId: Type.String({ minLength: 1 }),
|
|
88
|
+
enforcementGrade: EnforcementGradeUnion,
|
|
89
|
+
overlays: Type.Array(OverlayFlagUnion),
|
|
90
|
+
predecessorRef: Type.Optional(Type.String({ minLength: 1 })),
|
|
91
|
+
notes: Type.Optional(Type.String()),
|
|
92
|
+
},
|
|
93
|
+
{ additionalProperties: true },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
export type AdoptionStateRecordV2 = Static<typeof AdoptionStateRecordV2Schema>;
|
|
97
|
+
|
|
98
|
+
export type AdoptionStateRecord =
|
|
99
|
+
| AdoptionStateRecordV1
|
|
100
|
+
| AdoptionStateRecordV2;
|
|
101
|
+
|
|
102
|
+
export type AdoptionParseResult =
|
|
103
|
+
| { ok: true; kind: "v1"; record: AdoptionStateRecordV1 }
|
|
104
|
+
| { ok: true; kind: "v2"; record: AdoptionStateRecordV2 }
|
|
105
|
+
| { ok: false; error: string };
|
|
106
|
+
|
|
107
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
108
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse untrusted adoption JSON into an explicit v1 or v2 result.
|
|
113
|
+
* V1 never escalates to v2; missing schemaVersion 2 fields fail closed.
|
|
114
|
+
*/
|
|
115
|
+
export function parseAdoptionStateRecord(input: unknown): AdoptionParseResult {
|
|
116
|
+
if (!isRecord(input)) {
|
|
117
|
+
return { ok: false, error: "adoption record must be a JSON object" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input.schemaVersion === 2) {
|
|
121
|
+
if (!Value.Check(AdoptionStateRecordV2Schema, input)) {
|
|
122
|
+
return { ok: false, error: "invalid AdoptionStateRecordV2" };
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, kind: "v2", record: input };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (input.schemaVersion === 1) {
|
|
128
|
+
if (!Value.Check(AdoptionStateRecordV1Schema, input)) {
|
|
129
|
+
return { ok: false, error: "invalid AdoptionStateRecordV1" };
|
|
130
|
+
}
|
|
131
|
+
// Return only V1 fields so extra V2-shaped keys cannot imply graded claims.
|
|
132
|
+
const record: AdoptionStateRecordV1 = {
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
agentId: input.agentId as string,
|
|
135
|
+
state: input.state as AdoptionState,
|
|
136
|
+
constitutionHash: input.constitutionHash as string,
|
|
137
|
+
recordedAt: input.recordedAt as string,
|
|
138
|
+
};
|
|
139
|
+
if (typeof input.predecessorRef === "string") {
|
|
140
|
+
record.predecessorRef = input.predecessorRef;
|
|
141
|
+
}
|
|
142
|
+
if (typeof input.notes === "string") {
|
|
143
|
+
record.notes = input.notes;
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, kind: "v1", record };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: `unsupported adoption schema version: ${String(input.schemaVersion)}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned canonical JSON serializers.
|
|
3
|
+
*
|
|
4
|
+
* V1 preserves the historical sorted-key algorithm used by audit logs and
|
|
5
|
+
* claim signatures. V2 is RFC 8785-compatible (JCS) for new envelopes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Legacy v1 canonicalization: lexicographically sorted object keys,
|
|
10
|
+
* no insignificant whitespace, arrays preserve order, primitives via
|
|
11
|
+
* JSON.stringify. Must remain bit-identical forever for historical chains.
|
|
12
|
+
*/
|
|
13
|
+
export function canonicalizeV1(value: unknown): string {
|
|
14
|
+
if (value === null || typeof value !== "object") {
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return "[" + value.map(canonicalizeV1).join(",") + "]";
|
|
19
|
+
}
|
|
20
|
+
const obj = value as Record<string, unknown>;
|
|
21
|
+
const keys = Object.keys(obj).sort();
|
|
22
|
+
return (
|
|
23
|
+
"{" +
|
|
24
|
+
keys
|
|
25
|
+
.map((k) => JSON.stringify(k) + ":" + canonicalizeV1(obj[k]))
|
|
26
|
+
.join(",") +
|
|
27
|
+
"}"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* RFC 8785-compatible canonical JSON (JCS).
|
|
33
|
+
* - Object keys sorted by UTF-16 code unit order (JS string sort)
|
|
34
|
+
* - Undefined object members omitted; undefined array elements → null
|
|
35
|
+
* - Numbers via JSON.stringify (ECMAScript NumberToString)
|
|
36
|
+
* - No insignificant whitespace
|
|
37
|
+
*/
|
|
38
|
+
export function canonicalizeV2(value: unknown): string {
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
return "null";
|
|
41
|
+
}
|
|
42
|
+
if (value === null || typeof value !== "object") {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return (
|
|
47
|
+
"[" +
|
|
48
|
+
value
|
|
49
|
+
.map((item) =>
|
|
50
|
+
item === undefined ? "null" : canonicalizeV2(item),
|
|
51
|
+
)
|
|
52
|
+
.join(",") +
|
|
53
|
+
"]"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const obj = value as Record<string, unknown>;
|
|
57
|
+
const keys = Object.keys(obj)
|
|
58
|
+
.filter((k) => obj[k] !== undefined)
|
|
59
|
+
.sort();
|
|
60
|
+
return (
|
|
61
|
+
"{" +
|
|
62
|
+
keys
|
|
63
|
+
.map((k) => JSON.stringify(k) + ":" + canonicalizeV2(obj[k]))
|
|
64
|
+
.join(",") +
|
|
65
|
+
"}"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @deprecated Prefer canonicalizeV1 — alias for migration call sites. */
|
|
70
|
+
export const canonicalize = canonicalizeV1;
|
package/src/capsules.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust-state capsule schema (emission deferred to later plans).
|
|
3
|
+
* Plan 13: optional adoptionDisclosure summary for peer-facing grade/assurance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
7
|
+
import { Value } from "@sinclair/typebox/value";
|
|
8
|
+
import { FreshnessEnvelopeSchema } from "./freshness.js";
|
|
9
|
+
import { KEY_ALGORITHM } from "./identity.js";
|
|
10
|
+
import {
|
|
11
|
+
ADOPTION_OVERLAY_FLAGS,
|
|
12
|
+
ENFORCEMENT_GRADES,
|
|
13
|
+
} from "./adoption.js";
|
|
14
|
+
import { ADOPTION_ASSURANCE_CLASSES } from "./adoption-disclosure.js";
|
|
15
|
+
|
|
16
|
+
const OverlayFlagUnion = Type.Union(
|
|
17
|
+
ADOPTION_OVERLAY_FLAGS.map((f) => Type.Literal(f)) as [
|
|
18
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>>,
|
|
19
|
+
...ReturnType<
|
|
20
|
+
typeof Type.Literal<(typeof ADOPTION_OVERLAY_FLAGS)[number]>
|
|
21
|
+
>[],
|
|
22
|
+
],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const EnforcementGradeUnion = Type.Union(
|
|
26
|
+
ENFORCEMENT_GRADES.map((g) => Type.Literal(g)) as [
|
|
27
|
+
ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>,
|
|
28
|
+
...ReturnType<typeof Type.Literal<(typeof ENFORCEMENT_GRADES)[number]>>[],
|
|
29
|
+
],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const AssuranceUnion = Type.Union(
|
|
33
|
+
ADOPTION_ASSURANCE_CLASSES.map((a) => Type.Literal(a)) as [
|
|
34
|
+
ReturnType<typeof Type.Literal<(typeof ADOPTION_ASSURANCE_CLASSES)[number]>>,
|
|
35
|
+
...ReturnType<
|
|
36
|
+
typeof Type.Literal<(typeof ADOPTION_ASSURANCE_CLASSES)[number]>
|
|
37
|
+
>[],
|
|
38
|
+
],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** Compact peer-facing adoption summary (no raw MEMORY/SOUL). */
|
|
42
|
+
export const CapsuleAdoptionDisclosureSummarySchema = Type.Object(
|
|
43
|
+
{
|
|
44
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
45
|
+
harnessId: Type.String({ minLength: 1 }),
|
|
46
|
+
localState: Type.String({ minLength: 1 }),
|
|
47
|
+
enforcementGrade: EnforcementGradeUnion,
|
|
48
|
+
overlays: Type.Array(OverlayFlagUnion),
|
|
49
|
+
assurance: AssuranceUnion,
|
|
50
|
+
},
|
|
51
|
+
{ additionalProperties: false },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export type CapsuleAdoptionDisclosureSummary = Static<
|
|
55
|
+
typeof CapsuleAdoptionDisclosureSummarySchema
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
export const TrustStateCapsuleV2Schema = Type.Object(
|
|
59
|
+
{
|
|
60
|
+
schemaVersion: Type.Literal(2),
|
|
61
|
+
runtimeId: Type.String({ minLength: 1 }),
|
|
62
|
+
implementationVersion: Type.String({ minLength: 1 }),
|
|
63
|
+
evidenceRoot: Type.String({ minLength: 1 }),
|
|
64
|
+
coverage: Type.Object({
|
|
65
|
+
claims: Type.Integer({ minimum: 0 }),
|
|
66
|
+
receipts: Type.Integer({ minimum: 0 }),
|
|
67
|
+
completeness: Type.Union([
|
|
68
|
+
Type.Literal("none"),
|
|
69
|
+
Type.Literal("partial"),
|
|
70
|
+
Type.Literal("full"),
|
|
71
|
+
]),
|
|
72
|
+
}),
|
|
73
|
+
freshness: FreshnessEnvelopeSchema,
|
|
74
|
+
agentId: Type.String({ minLength: 1 }),
|
|
75
|
+
publicKey: Type.String({ minLength: 1 }),
|
|
76
|
+
signature: Type.String({ minLength: 1 }),
|
|
77
|
+
keyAlgorithm: Type.Literal(KEY_ALGORITHM),
|
|
78
|
+
adoptionDisclosure: Type.Optional(CapsuleAdoptionDisclosureSummarySchema),
|
|
79
|
+
},
|
|
80
|
+
{ additionalProperties: true },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export type TrustStateCapsuleV2 = Static<typeof TrustStateCapsuleV2Schema>;
|
|
84
|
+
|
|
85
|
+
export type CapsuleParseResult =
|
|
86
|
+
| { ok: true; capsule: TrustStateCapsuleV2 }
|
|
87
|
+
| { ok: false; error: string };
|
|
88
|
+
|
|
89
|
+
export function parseTrustStateCapsule(input: unknown): CapsuleParseResult {
|
|
90
|
+
if (!Value.Check(TrustStateCapsuleV2Schema, input)) {
|
|
91
|
+
return { ok: false, error: "invalid TrustStateCapsuleV2" };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, capsule: input };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type CapsuleAdoptionConsistency = {
|
|
97
|
+
ok: boolean;
|
|
98
|
+
error?: string | undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Peer-summary contract: if adoption is advertised, disclosure summary is
|
|
103
|
+
* required. Prompt-only / declaration-only cannot elevate completeness to full.
|
|
104
|
+
*/
|
|
105
|
+
export function validateCapsuleAdoptionConsistency(
|
|
106
|
+
input: unknown,
|
|
107
|
+
): CapsuleAdoptionConsistency {
|
|
108
|
+
if (!input || typeof input !== "object") {
|
|
109
|
+
return { ok: false, error: "capsule must be an object" };
|
|
110
|
+
}
|
|
111
|
+
const obj = input as Record<string, unknown>;
|
|
112
|
+
const advertising = obj.advertisingAdoption === true;
|
|
113
|
+
|
|
114
|
+
const disclosure = obj.adoptionDisclosure;
|
|
115
|
+
if (advertising && (disclosure === undefined || disclosure === null)) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error:
|
|
119
|
+
"peer-summary adoption advertisement requires adoptionDisclosure summary",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (disclosure === undefined || disclosure === null) {
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!Value.Check(CapsuleAdoptionDisclosureSummarySchema, disclosure)) {
|
|
128
|
+
return { ok: false, error: "invalid adoptionDisclosure summary" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const coverage = obj.coverage as
|
|
132
|
+
| { completeness?: string }
|
|
133
|
+
| undefined;
|
|
134
|
+
const completeness = coverage?.completeness;
|
|
135
|
+
if (
|
|
136
|
+
(disclosure.enforcementGrade === "prompt-only" ||
|
|
137
|
+
disclosure.assurance === "declaration-only") &&
|
|
138
|
+
completeness === "full"
|
|
139
|
+
) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error:
|
|
143
|
+
"prompt-only / declaration-only adoptionDisclosure cannot pair with completeness=full",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
disclosure.enforcementGrade === "tool-proxy" &&
|
|
149
|
+
completeness === "full"
|
|
150
|
+
) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
error:
|
|
154
|
+
"tool-proxy adoptionDisclosure is capped at partial completeness (grade=tool-proxy assurance=" +
|
|
155
|
+
disclosure.assurance +
|
|
156
|
+
")",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
package/src/claims.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned constitutional claim schemas and parsers.
|
|
3
|
+
*
|
|
4
|
+
* Untrusted peer JSON is runtime-validated. Legacy v1 claims are accepted
|
|
5
|
+
* only as declaration-only input and are never silently upgraded to v2.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
9
|
+
import { Value } from "@sinclair/typebox/value";
|
|
10
|
+
import { KEY_ALGORITHM } from "./identity.js";
|
|
11
|
+
import { FreshnessEnvelopeSchema } from "./freshness.js";
|
|
12
|
+
|
|
13
|
+
export const CLAIM_CLASSES = [
|
|
14
|
+
"identity",
|
|
15
|
+
"configuration",
|
|
16
|
+
"runtime",
|
|
17
|
+
"event",
|
|
18
|
+
"completeness",
|
|
19
|
+
"behavioral",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type ClaimClass = (typeof CLAIM_CLASSES)[number];
|
|
23
|
+
|
|
24
|
+
export const LegacyConstitutionalClaimV1Schema = Type.Object(
|
|
25
|
+
{
|
|
26
|
+
agentId: Type.String({ minLength: 1 }),
|
|
27
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
28
|
+
adoptedAt: Type.String({ minLength: 1 }),
|
|
29
|
+
auditMerkleRoot: Type.String({ minLength: 1 }),
|
|
30
|
+
auditEntryCount: Type.Integer({ minimum: 0 }),
|
|
31
|
+
chainIntact: Type.Boolean(),
|
|
32
|
+
recentLaws: Type.Array(Type.String()),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: true },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export type LegacyConstitutionalClaimV1 = Static<
|
|
38
|
+
typeof LegacyConstitutionalClaimV1Schema
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
export const ConstitutionalClaimV2Schema = Type.Object(
|
|
42
|
+
{
|
|
43
|
+
schemaVersion: Type.Literal(2),
|
|
44
|
+
claimClass: Type.Union(
|
|
45
|
+
CLAIM_CLASSES.map((c) => Type.Literal(c)) as [
|
|
46
|
+
ReturnType<typeof Type.Literal<(typeof CLAIM_CLASSES)[number]>>,
|
|
47
|
+
...ReturnType<typeof Type.Literal<(typeof CLAIM_CLASSES)[number]>>[],
|
|
48
|
+
],
|
|
49
|
+
),
|
|
50
|
+
agentId: Type.String({ pattern: "^fpp:ed25519:[0-9a-fA-F]{64}$" }),
|
|
51
|
+
keyAlgorithm: Type.Literal(KEY_ALGORITHM),
|
|
52
|
+
constitutionHash: Type.String({ minLength: 1 }),
|
|
53
|
+
adoptedAt: Type.String({ minLength: 1 }),
|
|
54
|
+
auditMerkleRoot: Type.String({ minLength: 1 }),
|
|
55
|
+
auditEntryCount: Type.Integer({ minimum: 0 }),
|
|
56
|
+
chainIntact: Type.Boolean(),
|
|
57
|
+
recentLaws: Type.Array(Type.String()),
|
|
58
|
+
/** When present, freshness fields are part of the signed canonical payload. */
|
|
59
|
+
freshness: Type.Optional(FreshnessEnvelopeSchema),
|
|
60
|
+
},
|
|
61
|
+
{ additionalProperties: true },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export type ConstitutionalClaimV2 = Static<typeof ConstitutionalClaimV2Schema>;
|
|
65
|
+
|
|
66
|
+
export type ClaimParseResult =
|
|
67
|
+
| {
|
|
68
|
+
ok: true;
|
|
69
|
+
kind: "legacy-v1";
|
|
70
|
+
assurance: "declaration-only";
|
|
71
|
+
claim: LegacyConstitutionalClaimV1;
|
|
72
|
+
diagnostics: string[];
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
ok: true;
|
|
76
|
+
kind: "v2";
|
|
77
|
+
assurance: "schema-validated";
|
|
78
|
+
claim: ConstitutionalClaimV2;
|
|
79
|
+
diagnostics: string[];
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
diagnostics: string[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse untrusted claim JSON into an explicit v1 or v2 result.
|
|
93
|
+
* Unknown critical versions fail closed. V1 never escalates to v2 assurance.
|
|
94
|
+
*/
|
|
95
|
+
export function parseClaim(input: unknown): ClaimParseResult {
|
|
96
|
+
if (!isRecord(input)) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: "claim must be a JSON object",
|
|
100
|
+
diagnostics: ["expected object"],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ("schemaVersion" in input) {
|
|
105
|
+
if (input.schemaVersion === 2) {
|
|
106
|
+
const errors = [...Value.Errors(ConstitutionalClaimV2Schema, input)];
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: "malformed constitutional claim v2",
|
|
111
|
+
diagnostics: errors.map((e) => `${e.path}: ${e.message}`),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
kind: "v2",
|
|
117
|
+
assurance: "schema-validated",
|
|
118
|
+
claim: input as ConstitutionalClaimV2,
|
|
119
|
+
diagnostics: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: `unsupported critical schema version: ${String(input.schemaVersion)}`,
|
|
125
|
+
diagnostics: [`schemaVersion=${String(input.schemaVersion)}`],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const errors = [...Value.Errors(LegacyConstitutionalClaimV1Schema, input)];
|
|
130
|
+
if (errors.length > 0) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: "malformed legacy constitutional claim v1",
|
|
134
|
+
diagnostics: errors.map((e) => `${e.path}: ${e.message}`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
kind: "legacy-v1",
|
|
141
|
+
assurance: "declaration-only",
|
|
142
|
+
claim: input as LegacyConstitutionalClaimV1,
|
|
143
|
+
diagnostics: [
|
|
144
|
+
"legacy-v1 accepted as declaration-only; not escalated to v2 assurance",
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|