@fluxpointstudios/orynq-sdk-process-trace 0.2.0 → 0.3.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/LICENSE +21 -21
- package/dist/index.cjs +574 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +573 -9
- package/dist/index.d.ts +573 -9
- package/dist/index.js +562 -9
- package/dist/index.js.map +1 -1
- package/package.json +17 -3
- package/src/__tests__/bundle.test.ts +942 -860
- package/src/__tests__/governance-round4.test.ts +149 -0
- package/src/__tests__/governance.test.ts +382 -0
- package/src/__tests__/hardening-round2.test.ts +173 -0
- package/src/__tests__/hardening-round3.test.ts +208 -0
- package/src/__tests__/integration.test.ts +611 -611
- package/src/__tests__/merkle.test.ts +756 -756
- package/src/__tests__/model-manifest.test.ts +136 -0
- package/src/__tests__/rolling-hash.test.ts +622 -622
- package/src/__tests__/trace-builder.test.ts +1012 -1012
- package/src/__tests__/types.test.ts +420 -414
- package/src/bundle.ts +1004 -810
- package/src/disclosure.ts +527 -527
- package/src/governance.ts +711 -0
- package/src/index.ts +334 -265
- package/src/manifest.ts +687 -687
- package/src/merkle.ts +428 -428
- package/src/model-manifest.ts +0 -0
- package/src/rolling-hash.ts +375 -366
- package/src/trace-builder.ts +791 -725
- package/src/types.ts +713 -522
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @summary Round-2 hardening regression tests for process-trace (#58, #59).
|
|
3
|
+
*
|
|
4
|
+
* 1. eip712 governance verifier must pin/validate the attacker-controlled
|
|
5
|
+
* `event.eip712.{domain,primaryType,types}` against the expected
|
|
6
|
+
* governance-attestation schema — an unexpected domain/primaryType is
|
|
7
|
+
* rejected even when the raw signature "verifies".
|
|
8
|
+
* 2. verifyBundle must also verify the publicView model-manifest fields (the
|
|
9
|
+
* shared artifact an external verifier reads), not just privateRun's — a
|
|
10
|
+
* fabricated publicView.modelManifestHash must fail.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
createTrace,
|
|
16
|
+
addSpan,
|
|
17
|
+
addEvent,
|
|
18
|
+
closeSpan,
|
|
19
|
+
finalizeTrace,
|
|
20
|
+
verifyBundle,
|
|
21
|
+
verifyGovernanceAttestations,
|
|
22
|
+
createEip712GovernanceVerifier,
|
|
23
|
+
} from "../index.js";
|
|
24
|
+
import type { TraceBundle, TraceRun } from "../index.js";
|
|
25
|
+
|
|
26
|
+
const EXPECTED_DOMAIN = { name: "Orynq", version: "1" };
|
|
27
|
+
const EXPECTED_TYPES = {
|
|
28
|
+
Attestation: [
|
|
29
|
+
{ name: "role", type: "string" },
|
|
30
|
+
{ name: "policyRef", type: "string" },
|
|
31
|
+
{ name: "decisionRef", type: "string" },
|
|
32
|
+
{ name: "runId", type: "string" },
|
|
33
|
+
{ name: "signedAt", type: "string" },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function eip712Bundle(bindingOverrides: Record<string, unknown>): Promise<TraceBundle> {
|
|
38
|
+
const run = await createTrace({ agentId: "agent-1" });
|
|
39
|
+
const span = addSpan(run, { name: "approve", visibility: "public" });
|
|
40
|
+
const signedAt = new Date().toISOString();
|
|
41
|
+
await addEvent(run, span.id, {
|
|
42
|
+
kind: "governance-attestation",
|
|
43
|
+
visibility: "public",
|
|
44
|
+
role: "compliance",
|
|
45
|
+
policyRef: "sha256:policy",
|
|
46
|
+
decisionRef: "decision-1",
|
|
47
|
+
attestor: {
|
|
48
|
+
address: "0x1111111111111111111111111111111111111111",
|
|
49
|
+
signatureScheme: "eip712",
|
|
50
|
+
},
|
|
51
|
+
signature: "0x" + "ab".repeat(65),
|
|
52
|
+
signedAt,
|
|
53
|
+
eip712: {
|
|
54
|
+
domain: EXPECTED_DOMAIN,
|
|
55
|
+
types: EXPECTED_TYPES,
|
|
56
|
+
primaryType: "Attestation",
|
|
57
|
+
message: {
|
|
58
|
+
role: "compliance",
|
|
59
|
+
policyRef: "sha256:policy",
|
|
60
|
+
decisionRef: "decision-1",
|
|
61
|
+
runId: run.id,
|
|
62
|
+
signedAt,
|
|
63
|
+
},
|
|
64
|
+
...bindingOverrides,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
await closeSpan(run, span.id);
|
|
68
|
+
return finalizeTrace(run);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("eip712 domain/primaryType pinning (#58)", () => {
|
|
72
|
+
const ATTESTOR = "0x1111111111111111111111111111111111111111";
|
|
73
|
+
|
|
74
|
+
it("rejects an unexpected primaryType even when the raw signature verifies", async () => {
|
|
75
|
+
const bundle = await eip712Bundle({ primaryType: "EvilType" });
|
|
76
|
+
const verifier = createEip712GovernanceVerifier({
|
|
77
|
+
verifyTypedData: async () => true, // signature is 'valid' over attacker data
|
|
78
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
79
|
+
expectedPrimaryType: "Attestation",
|
|
80
|
+
expectedTypes: EXPECTED_TYPES,
|
|
81
|
+
});
|
|
82
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
83
|
+
verifiers: { eip712: verifier },
|
|
84
|
+
authorizedAttestors: [ATTESTOR],
|
|
85
|
+
});
|
|
86
|
+
expect(summaries[0]!.authorized).toBe(true);
|
|
87
|
+
expect(summaries[0]!.verified).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rejects an unexpected domain (attacker swaps verifyingContract/chainId)", async () => {
|
|
91
|
+
const bundle = await eip712Bundle({
|
|
92
|
+
domain: { name: "Orynq", version: "1", chainId: 1, verifyingContract: "0xevil" },
|
|
93
|
+
});
|
|
94
|
+
const verifier = createEip712GovernanceVerifier({
|
|
95
|
+
verifyTypedData: async () => true,
|
|
96
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
97
|
+
expectedPrimaryType: "Attestation",
|
|
98
|
+
expectedTypes: EXPECTED_TYPES,
|
|
99
|
+
});
|
|
100
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
101
|
+
verifiers: { eip712: verifier },
|
|
102
|
+
authorizedAttestors: [ATTESTOR],
|
|
103
|
+
});
|
|
104
|
+
expect(summaries[0]!.authorized).toBe(true);
|
|
105
|
+
expect(summaries[0]!.verified).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("accepts the expected domain + primaryType", async () => {
|
|
109
|
+
const bundle = await eip712Bundle({});
|
|
110
|
+
const verifier = createEip712GovernanceVerifier({
|
|
111
|
+
verifyTypedData: async () => true,
|
|
112
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
113
|
+
expectedPrimaryType: "Attestation",
|
|
114
|
+
expectedTypes: EXPECTED_TYPES,
|
|
115
|
+
});
|
|
116
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
117
|
+
verifiers: { eip712: verifier },
|
|
118
|
+
authorizedAttestors: [ATTESTOR],
|
|
119
|
+
});
|
|
120
|
+
expect(summaries[0]!.verified).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("publicView model-manifest verification (#59)", () => {
|
|
125
|
+
async function manifestBundle(): Promise<TraceBundle> {
|
|
126
|
+
const run: TraceRun = await createTrace({
|
|
127
|
+
agentId: "agent-m",
|
|
128
|
+
manifest: {
|
|
129
|
+
modelHash: "sha256:" + "1".repeat(64),
|
|
130
|
+
modelId: "gpt-x",
|
|
131
|
+
framework: "acme",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const span = addSpan(run, { name: "work", visibility: "public" });
|
|
135
|
+
await addEvent(run, span.id, { kind: "command", command: "go", visibility: "public" });
|
|
136
|
+
await closeSpan(run, span.id);
|
|
137
|
+
return finalizeTrace(run);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
it("an honest bundle with a pinned manifest verifies", async () => {
|
|
141
|
+
const bundle = await manifestBundle();
|
|
142
|
+
expect(bundle.publicView.modelManifestHash).toBeDefined();
|
|
143
|
+
const result = await verifyBundle(bundle, { });
|
|
144
|
+
expect(result.checks.modelManifestValid).toBe(true);
|
|
145
|
+
expect(result.valid).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("a fabricated publicView.modelManifestHash fails verification", async () => {
|
|
149
|
+
const bundle = await manifestBundle();
|
|
150
|
+
const forged = JSON.parse(JSON.stringify(bundle)) as TraceBundle;
|
|
151
|
+
// privateRun manifest is left honest; only the shared publicView field an
|
|
152
|
+
// external verifier reads is fabricated.
|
|
153
|
+
forged.publicView.modelManifestHash = "f".repeat(64);
|
|
154
|
+
|
|
155
|
+
const result = await verifyBundle(forged);
|
|
156
|
+
expect(result.checks.modelManifestValid).toBe(false);
|
|
157
|
+
expect(result.valid).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("a fabricated publicView.modelManifest (recomputes to a different hash) fails", async () => {
|
|
161
|
+
const bundle = await manifestBundle();
|
|
162
|
+
const forged = JSON.parse(JSON.stringify(bundle)) as TraceBundle;
|
|
163
|
+
(forged.publicView.modelManifest as Record<string, unknown>) = {
|
|
164
|
+
modelHash: "sha256:" + "9".repeat(64),
|
|
165
|
+
modelId: "EVIL",
|
|
166
|
+
framework: "attacker",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = await verifyBundle(forged);
|
|
170
|
+
expect(result.checks.modelManifestValid).toBe(false);
|
|
171
|
+
expect(result.valid).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @summary Round-3 hardening regression tests for process-trace governance (#58).
|
|
3
|
+
*
|
|
4
|
+
* 1. eip712 schema pins are MANDATORY. An attacker signs an EMPTY `Attestation`
|
|
5
|
+
* struct (`types:{Attestation:[]}`) with their own key and carries
|
|
6
|
+
* role/policyRef/decisionRef/runId as UNTYPED message extras — viem verifies
|
|
7
|
+
* the empty struct and the field-binding post-check reads never-signed
|
|
8
|
+
* fields. The verifier must reject unless the primaryType's declared type
|
|
9
|
+
* fields actually include role/policyRef/decisionRef/runId.
|
|
10
|
+
* 2. Governance attestations require an authorized-attestor allow-list. An
|
|
11
|
+
* attestation from an unlisted signer must NOT count toward a passing
|
|
12
|
+
* verdict, and with no allow-list governance verification must fail closed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from "vitest";
|
|
16
|
+
import {
|
|
17
|
+
createTrace,
|
|
18
|
+
addSpan,
|
|
19
|
+
addEvent,
|
|
20
|
+
closeSpan,
|
|
21
|
+
finalizeTrace,
|
|
22
|
+
verifyBundle,
|
|
23
|
+
addGovernanceAttestation,
|
|
24
|
+
createSr25519GovernanceSigner,
|
|
25
|
+
verifyGovernanceAttestations,
|
|
26
|
+
createEip712GovernanceVerifier,
|
|
27
|
+
} from "../index.js";
|
|
28
|
+
import type { TraceBundle, GovernanceAttestationEvent } from "../index.js";
|
|
29
|
+
|
|
30
|
+
const SEED_A = "0x" + "11".repeat(32);
|
|
31
|
+
const SEED_B = "0x" + "22".repeat(32);
|
|
32
|
+
|
|
33
|
+
const EXPECTED_DOMAIN = { name: "Orynq", version: "1" };
|
|
34
|
+
const EXPECTED_TYPES = {
|
|
35
|
+
Attestation: [
|
|
36
|
+
{ name: "role", type: "string" },
|
|
37
|
+
{ name: "policyRef", type: "string" },
|
|
38
|
+
{ name: "decisionRef", type: "string" },
|
|
39
|
+
{ name: "runId", type: "string" },
|
|
40
|
+
{ name: "signedAt", type: "string" },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async function eip712Bundle(binding: Record<string, unknown>): Promise<TraceBundle> {
|
|
45
|
+
const run = await createTrace({ agentId: "agent-1" });
|
|
46
|
+
const span = addSpan(run, { name: "approve", visibility: "public" });
|
|
47
|
+
const signedAt = new Date().toISOString();
|
|
48
|
+
await addEvent(run, span.id, {
|
|
49
|
+
kind: "governance-attestation",
|
|
50
|
+
visibility: "public",
|
|
51
|
+
role: "release-authority",
|
|
52
|
+
policyRef: "sha256:policy",
|
|
53
|
+
decisionRef: "decision-1",
|
|
54
|
+
attestor: {
|
|
55
|
+
address: "0x1111111111111111111111111111111111111111",
|
|
56
|
+
signatureScheme: "eip712",
|
|
57
|
+
},
|
|
58
|
+
signature: "0x" + "ab".repeat(65),
|
|
59
|
+
signedAt,
|
|
60
|
+
eip712: {
|
|
61
|
+
domain: EXPECTED_DOMAIN,
|
|
62
|
+
types: EXPECTED_TYPES,
|
|
63
|
+
primaryType: "Attestation",
|
|
64
|
+
message: {
|
|
65
|
+
role: "release-authority",
|
|
66
|
+
policyRef: "sha256:policy",
|
|
67
|
+
decisionRef: "decision-1",
|
|
68
|
+
runId: run.id,
|
|
69
|
+
signedAt,
|
|
70
|
+
},
|
|
71
|
+
...binding,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
await closeSpan(run, span.id);
|
|
75
|
+
return finalizeTrace(run);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe("eip712 schema pins are mandatory (#58 round-3)", () => {
|
|
79
|
+
it("rejects an EMPTY Attestation struct that carries fields as untyped extras", async () => {
|
|
80
|
+
// The signed struct declares NO fields; role/policyRef/decisionRef/runId ride
|
|
81
|
+
// as untyped message extras the signature does not commit to.
|
|
82
|
+
const bundle = await eip712Bundle({
|
|
83
|
+
types: { Attestation: [] },
|
|
84
|
+
});
|
|
85
|
+
const verifier = createEip712GovernanceVerifier({
|
|
86
|
+
verifyTypedData: async () => true, // empty struct 'verifies' with attacker key
|
|
87
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
88
|
+
expectedPrimaryType: "Attestation",
|
|
89
|
+
expectedTypes: EXPECTED_TYPES,
|
|
90
|
+
});
|
|
91
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
92
|
+
verifiers: { eip712: verifier },
|
|
93
|
+
authorizedAttestors: ["0x1111111111111111111111111111111111111111"],
|
|
94
|
+
});
|
|
95
|
+
expect(summaries[0]!.verified).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("throws at construction when the schema pins are omitted", () => {
|
|
99
|
+
expect(() =>
|
|
100
|
+
// @ts-expect-error — pins are now mandatory
|
|
101
|
+
createEip712GovernanceVerifier({ verifyTypedData: async () => true })
|
|
102
|
+
).toThrow(/expectedTypes|expectedDomain|expectedPrimaryType|required/i);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws when the pinned primaryType omits a required field", () => {
|
|
106
|
+
expect(() =>
|
|
107
|
+
createEip712GovernanceVerifier({
|
|
108
|
+
verifyTypedData: async () => true,
|
|
109
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
110
|
+
expectedPrimaryType: "Attestation",
|
|
111
|
+
expectedTypes: {
|
|
112
|
+
Attestation: [
|
|
113
|
+
{ name: "role", type: "string" },
|
|
114
|
+
{ name: "policyRef", type: "string" },
|
|
115
|
+
// decisionRef + runId missing → signature would not commit to them
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
).toThrow(/decisionRef|runId|must include/i);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("accepts a correctly-typed genuine attestation", async () => {
|
|
123
|
+
const bundle = await eip712Bundle({});
|
|
124
|
+
const verifier = createEip712GovernanceVerifier({
|
|
125
|
+
verifyTypedData: async () => true,
|
|
126
|
+
expectedDomain: EXPECTED_DOMAIN,
|
|
127
|
+
expectedPrimaryType: "Attestation",
|
|
128
|
+
expectedTypes: EXPECTED_TYPES,
|
|
129
|
+
});
|
|
130
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
131
|
+
verifiers: { eip712: verifier },
|
|
132
|
+
authorizedAttestors: ["0x1111111111111111111111111111111111111111"],
|
|
133
|
+
});
|
|
134
|
+
expect(summaries[0]!.verified).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("governance requires an authorized-attestor allow-list (#58 round-3)", () => {
|
|
139
|
+
async function attestedBundle(): Promise<{ bundle: TraceBundle; attestor: string }> {
|
|
140
|
+
const run = await createTrace({ agentId: "agent-1" });
|
|
141
|
+
const span = addSpan(run, { name: "release", visibility: "public" });
|
|
142
|
+
const decision = await addEvent(run, span.id, {
|
|
143
|
+
kind: "decision",
|
|
144
|
+
decision: "ship model v2",
|
|
145
|
+
visibility: "public",
|
|
146
|
+
});
|
|
147
|
+
const signer = await createSr25519GovernanceSigner({ seed: SEED_A });
|
|
148
|
+
await addGovernanceAttestation(run, span.id, {
|
|
149
|
+
role: "release-authority",
|
|
150
|
+
policyRef: "sha256:policy-doc-hash",
|
|
151
|
+
decisionRef: decision.id,
|
|
152
|
+
signer,
|
|
153
|
+
});
|
|
154
|
+
await closeSpan(run, span.id);
|
|
155
|
+
return { bundle: await finalizeTrace(run), attestor: signer.address };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it("an attestation from an UNLISTED signer is not authorized", async () => {
|
|
159
|
+
const { bundle } = await attestedBundle();
|
|
160
|
+
const otherSigner = await createSr25519GovernanceSigner({ seed: SEED_B });
|
|
161
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
162
|
+
authorizedAttestors: [otherSigner.address], // genuine signer NOT listed
|
|
163
|
+
});
|
|
164
|
+
expect(summaries[0]!.verified).toBe(false);
|
|
165
|
+
expect(summaries[0]!.authorized).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("an attestation from an ALLOW-LISTED signer verifies", async () => {
|
|
169
|
+
const { bundle, attestor } = await attestedBundle();
|
|
170
|
+
const summaries = await verifyGovernanceAttestations(bundle, {
|
|
171
|
+
authorizedAttestors: [attestor],
|
|
172
|
+
});
|
|
173
|
+
expect(summaries[0]!.verified).toBe(true);
|
|
174
|
+
expect(summaries[0]!.authorized).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("with NO allow-list, governance fails closed and does not pass the bundle", async () => {
|
|
178
|
+
const { bundle } = await attestedBundle();
|
|
179
|
+
// A genuine self-signed attestation with a valid signature — but nobody said
|
|
180
|
+
// this key is a real release-authority. It must not fold into a pass.
|
|
181
|
+
const summaries = await verifyGovernanceAttestations(bundle);
|
|
182
|
+
expect(summaries[0]!.verified).toBe(false);
|
|
183
|
+
expect(summaries[0]!.authorized).toBe(false);
|
|
184
|
+
|
|
185
|
+
const result = await verifyBundle(bundle, { governance: true });
|
|
186
|
+
expect(result.checks.governanceValid).toBe(false);
|
|
187
|
+
expect(result.valid).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("verifyBundle with an allow-list folds a real sign-off into valid", async () => {
|
|
191
|
+
const { bundle, attestor } = await attestedBundle();
|
|
192
|
+
const result = await verifyBundle(bundle, {
|
|
193
|
+
governance: { authorizedAttestors: [attestor] },
|
|
194
|
+
});
|
|
195
|
+
expect(result.checks.governanceValid).toBe(true);
|
|
196
|
+
expect(result.valid).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("an unlisted signer fails the bundle", async () => {
|
|
200
|
+
const { bundle } = await attestedBundle();
|
|
201
|
+
const other = await createSr25519GovernanceSigner({ seed: SEED_B });
|
|
202
|
+
const result = await verifyBundle(bundle, {
|
|
203
|
+
governance: { authorizedAttestors: [other.address] },
|
|
204
|
+
});
|
|
205
|
+
expect(result.checks.governanceValid).toBe(false);
|
|
206
|
+
expect(result.valid).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|