sec-kn-4-delegation-principal-binding.test.mjs
974 lines 37.9 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago
1 /**
2 * SEC-KN-4 — seven-tier coverage for delegation principal binding at apply + authorship.
3 *
4 * Frozen spec: docs/SEC-KN-4-DELEGATION-PRINCIPAL-BINDING-FREEZE.md (R1–R9)
5 */
6
7 import { test, describe } from 'node:test';
8 import assert from 'node:assert/strict';
9 import fs from 'node:fs';
10 import os from 'node:os';
11 import path from 'node:path';
12 import { performance } from 'node:perf_hooks';
13 import { fileURLToPath } from 'node:url';
14 import { execSync } from 'node:child_process';
15
16 import {
17 hashPrincipalRef,
18 precheckApprovedDelegationProposal,
19 applyDelegationProposalToIndex,
20 handleDelegationGrantMintRequest,
21 handleAgentIdentityRegisterProposeRequest,
22 seedDelegationFixtures,
23 getConsent,
24 getAgentIdentity,
25 DELEGATION_PROPOSAL_SOURCE,
26 DELEGATION_CONSENTS_FILE,
27 DELEGATION_IDENTITIES_FILE,
28 DELEGATION_POLICY_FILE,
29 validateAgentIdentityRecord,
30 validateConsentRecord,
31 } from '../lib/agent/delegation.mjs';
32 import {
33 applyApprovedDelegationProposalFromCanister,
34 mergeDelegationFrontmatter,
35 normalizeCanisterProposalForDelegationPrecheck,
36 } from '../lib/agent/delegation-hosted-proposal.mjs';
37 import { createProposal } from '../hub/proposals-store.mjs';
38 import { matchesScoolingReviewTrayFingerprint } from '../lib/hub-proposal-personal-self-apply.mjs';
39 import {
40 writeDelegationPolicy,
41 makeAgentIdentity,
42 makeDelegationConsent,
43 TEST_USER_ID,
44 TEST_PRINCIPAL_REF,
45 } from './fixtures/agent/delegation-helpers.mjs';
46
47 const __dirname = path.dirname(fileURLToPath(import.meta.url));
48 const ROOT = path.resolve(__dirname, '..');
49 const MIGRATION_MO = path.join(ROOT, 'hub/icp/src/hub/Migration.mo');
50 const MAIN_MO = path.join(ROOT, 'hub/icp/src/hub/main.mo');
51 const DELEGATION_ROUTES = path.join(ROOT, 'hub/bridge/delegation-routes.mjs');
52 const HOSTED_PROPOSAL_SRC = path.join(ROOT, 'lib/agent/delegation-hosted-proposal.mjs');
53 const SERVER_SRC = path.join(ROOT, 'hub/server.mjs');
54
55 const ATTACKER_USER = 'attacker-user-id';
56 const VICTIM_USER = 'victim-user-id';
57 const ATTACKER_PRINCIPAL = hashPrincipalRef(ATTACKER_USER);
58 const VICTIM_PRINCIPAL = hashPrincipalRef(VICTIM_USER);
59 const PARTITION_OWNER = 'workspace-owner-uid';
60
61 function mkDataDir() {
62 return fs.mkdtempSync(path.join(os.tmpdir(), 'kt-sec-kn-4-'));
63 }
64
65 function enableGate(dataDir) {
66 writeDelegationPolicy(dataDir);
67 process.env.DELEGATION_ENABLED = '1';
68 }
69
70 function delegationProposal(dataDir, body, meta, authorFields = {}) {
71 return {
72 proposal_id: 'prop-sec-kn-4',
73 source: DELEGATION_PROPOSAL_SOURCE,
74 vault_id: 'default',
75 body: JSON.stringify(body),
76 delegation_meta: meta,
77 ...authorFields,
78 };
79 }
80
81 /**
82 * Pre-fix apply path (body-trusted) — replica of delegation.mjs:846-877 before SEC-KN-4.
83 *
84 * @param {string} dataDir
85 * @param {object} proposal
86 */
87 function precheckLegacyBodyTrusted(dataDir, proposal) {
88 if (proposal.source !== DELEGATION_PROPOSAL_SOURCE) {
89 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'Not a delegation proposal' };
90 }
91 const meta = proposal.delegation_meta;
92 if (!meta || typeof meta !== 'object' || typeof meta.record_kind !== 'string') {
93 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'Missing delegation_meta' };
94 }
95 let record;
96 try {
97 record = JSON.parse(proposal.body ?? '{}');
98 } catch {
99 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'Proposal body is not valid JSON' };
100 }
101 const vaultId =
102 typeof proposal.vault_id === 'string' && proposal.vault_id.trim()
103 ? proposal.vault_id.trim()
104 : 'default';
105 if (meta.record_kind === 'agent_identity') {
106 const v = validateAgentIdentityRecord(record);
107 if (!v.ok) return { ok: false, status: 400, code: 'BAD_REQUEST', error: v.error };
108 const existing = getAgentIdentity(dataDir, vaultId, record.agent_id);
109 if (existing) {
110 return { ok: false, status: 409, code: 'CONFLICT', error: 'Agent identity already registered' };
111 }
112 } else if (meta.record_kind === 'delegation_consent') {
113 const v = validateConsentRecord(record);
114 if (!v.ok) return { ok: false, status: 400, code: 'BAD_REQUEST', error: v.error };
115 record.evidence_ref = `proposal:${proposal.proposal_id}`;
116 const identity = getAgentIdentity(dataDir, vaultId, record.delegate_agent_id);
117 if (!identity || identity.status !== 'active') {
118 return { ok: false, status: 403, code: 'DELEGATION_IDENTITY_DENIED', error: 'Delegate agent not active' };
119 }
120 } else {
121 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'Unknown delegation record kind' };
122 }
123 return { ok: true, vaultId, recordKind: meta.record_kind, record };
124 }
125
126 // ---------------------------------------------------------------------------
127 // Tier 1 — unit
128 // ---------------------------------------------------------------------------
129 describe('SEC-KN-4 unit — principal binding + author gate', () => {
130 test('hashPrincipalRef is deterministic for the author', () => {
131 assert.equal(hashPrincipalRef(TEST_USER_ID), TEST_PRINCIPAL_REF);
132 assert.equal(hashPrincipalRef(TEST_USER_ID), hashPrincipalRef(TEST_USER_ID));
133 });
134
135 test('R3 mismatch → DELEGATION_PRINCIPAL_REBIND_MISMATCH', () => {
136 const dir = mkDataDir();
137 try {
138 enableGate(dir);
139 const identity = makeAgentIdentity({ agentId: 'agent_mismatch01' });
140 seedDelegationFixtures(dir, 'default', identity);
141 const body = makeDelegationConsent({
142 consentId: 'dcons_mismatch01',
143 agentId: identity.agent_id,
144 });
145 body.principal_ref = VICTIM_PRINCIPAL;
146 const proposal = delegationProposal(dir, body, {
147 record_kind: 'delegation_consent',
148 consent_id: body.consent_id,
149 });
150 const result = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
151 assert.equal(result.ok, false);
152 assert.equal(result.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
153 } finally {
154 delete process.env.DELEGATION_ENABLED;
155 fs.rmSync(dir, { recursive: true, force: true });
156 }
157 });
158
159 test('R3 match → applied record principal_ref equals derived', () => {
160 const dir = mkDataDir();
161 try {
162 enableGate(dir);
163 const identity = makeAgentIdentity({ agentId: 'agent_match_test01' });
164 seedDelegationFixtures(dir, 'default', identity);
165 const body = makeDelegationConsent({
166 consentId: 'dcons_match_test01',
167 agentId: identity.agent_id,
168 });
169 const proposal = delegationProposal(dir, body, {
170 record_kind: 'delegation_consent',
171 consent_id: body.consent_id,
172 });
173 const result = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
174 assert.equal(result.ok, true);
175 assert.equal(result.record.principal_ref, TEST_PRINCIPAL_REF);
176 } finally {
177 delete process.env.DELEGATION_ENABLED;
178 fs.rmSync(dir, { recursive: true, force: true });
179 }
180 });
181
182 test('R4 owner_ref mismatch → DELEGATION_OWNER_REBIND_MISMATCH', () => {
183 const dir = mkDataDir();
184 try {
185 enableGate(dir);
186 const body = makeAgentIdentity({ agentId: 'agent_owner_mm01' });
187 body.owner_ref = VICTIM_PRINCIPAL;
188 const proposal = delegationProposal(dir, body, {
189 record_kind: 'agent_identity',
190 agent_id: body.agent_id,
191 });
192 const result = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
193 assert.equal(result.ok, false);
194 assert.equal(result.code, 'DELEGATION_OWNER_REBIND_MISMATCH');
195 } finally {
196 delete process.env.DELEGATION_ENABLED;
197 fs.rmSync(dir, { recursive: true, force: true });
198 }
199 });
200
201 test('R5 org_ref principal and owner → DELEGATION_ORG_REF_UNSUPPORTED', () => {
202 const dir = mkDataDir();
203 try {
204 enableGate(dir);
205 const identity = makeAgentIdentity({ agentId: 'agent_org01' });
206 seedDelegationFixtures(dir, 'default', identity);
207 const consentBody = makeDelegationConsent({
208 consentId: 'dcons_org01',
209 agentId: identity.agent_id,
210 });
211 consentBody.principal_ref = 'org_ref:ws_evil';
212 const consentProposal = delegationProposal(dir, consentBody, {
213 record_kind: 'delegation_consent',
214 consent_id: consentBody.consent_id,
215 });
216 const consentResult = precheckApprovedDelegationProposal(dir, consentProposal, {
217 author: TEST_USER_ID,
218 });
219 assert.equal(consentResult.code, 'DELEGATION_ORG_REF_UNSUPPORTED');
220
221 const idBody = makeAgentIdentity({ agentId: 'agent_org02' });
222 idBody.owner_ref = 'org_ref:ws_evil';
223 const idProposal = delegationProposal(dir, idBody, {
224 record_kind: 'agent_identity',
225 agent_id: idBody.agent_id,
226 });
227 const idResult = precheckApprovedDelegationProposal(dir, idProposal, { author: TEST_USER_ID });
228 assert.equal(idResult.code, 'DELEGATION_ORG_REF_UNSUPPORTED');
229
230 // R5 is worded on the body's principal_ref OR owner_ref, independent of kind.
231 // A consent body smuggling org_ref in owner_ref must refuse too, even though
232 // validateConsentRecord never reads owner_ref.
233 const crossBody = makeDelegationConsent({
234 consentId: 'dcons_org02',
235 agentId: identity.agent_id,
236 });
237 crossBody.owner_ref = 'org_ref:ws_evil';
238 const crossProposal = delegationProposal(dir, crossBody, {
239 record_kind: 'delegation_consent',
240 consent_id: crossBody.consent_id,
241 });
242 const crossResult = precheckApprovedDelegationProposal(dir, crossProposal, {
243 author: TEST_USER_ID,
244 });
245 assert.equal(crossResult.code, 'DELEGATION_ORG_REF_UNSUPPORTED');
246 } finally {
247 delete process.env.DELEGATION_ENABLED;
248 fs.rmSync(dir, { recursive: true, force: true });
249 }
250 });
251
252 test('R2 author failures → DELEGATION_AUTHOR_UNVERIFIED', () => {
253 const dir = mkDataDir();
254 try {
255 enableGate(dir);
256 const identity = makeAgentIdentity({ agentId: 'agent_auth01' });
257 seedDelegationFixtures(dir, 'default', identity);
258 const body = makeDelegationConsent({
259 consentId: 'dcons_auth01',
260 agentId: identity.agent_id,
261 });
262 const proposal = delegationProposal(dir, body, {
263 record_kind: 'delegation_consent',
264 consent_id: body.consent_id,
265 });
266 for (const author of ['', ' ', 'x'.repeat(129), 'bad char!']) {
267 const result = precheckApprovedDelegationProposal(dir, proposal, { author });
268 assert.equal(result.code, 'DELEGATION_AUTHOR_UNVERIFIED', `author=${JSON.stringify(author)}`);
269 }
270 assert.equal(
271 precheckApprovedDelegationProposal(dir, proposal, undefined).code,
272 'DELEGATION_AUTHOR_UNVERIFIED',
273 );
274 assert.equal(
275 precheckApprovedDelegationProposal(dir, { ...proposal, _knowtation_backup_json_unparseable: true }, {
276 author: TEST_USER_ID,
277 }).code,
278 'DELEGATION_AUTHOR_UNVERIFIED',
279 );
280 } finally {
281 delete process.env.DELEGATION_ENABLED;
282 fs.rmSync(dir, { recursive: true, force: true });
283 }
284 });
285
286 test('R7 gate off → DELEGATION_DISABLED; policy forbidden → DELEGATION_POLICY_FORBIDDEN', () => {
287 const dir = mkDataDir();
288 try {
289 delete process.env.DELEGATION_ENABLED;
290 const identity = makeAgentIdentity({ agentId: 'agent_gate01' });
291 seedDelegationFixtures(dir, 'default', identity);
292 const body = makeDelegationConsent({
293 consentId: 'dcons_gate01',
294 agentId: identity.agent_id,
295 });
296 const proposal = delegationProposal(dir, body, {
297 record_kind: 'delegation_consent',
298 consent_id: body.consent_id,
299 });
300 const off = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
301 assert.equal(off.code, 'DELEGATION_DISABLED');
302
303 fs.writeFileSync(
304 path.join(dir, 'hub_delegation_policy.json'),
305 JSON.stringify({ delegation: { enabled: true, forbidden: true } }),
306 'utf8',
307 );
308 process.env.DELEGATION_ENABLED = '1';
309 const forbidden = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
310 assert.equal(forbidden.code, 'DELEGATION_POLICY_FORBIDDEN');
311 } finally {
312 delete process.env.DELEGATION_ENABLED;
313 fs.rmSync(dir, { recursive: true, force: true });
314 }
315 });
316 });
317
318 // ---------------------------------------------------------------------------
319 // Tier 2 — integration
320 // ---------------------------------------------------------------------------
321 describe('SEC-KN-4 integration — call sites + canister contracts', () => {
322 test('self-hosted approve passes proposal.proposed_by as author', () => {
323 const src = fs.readFileSync(SERVER_SRC, 'utf8');
324 assert.match(src, /precheckApprovedDelegationProposal\(config\.data_dir, proposal, \{/);
325 assert.match(src, /author: typeof proposal\.proposed_by === 'string' \? proposal\.proposed_by : ''/);
326 });
327
328 test('hosted apply passes canister created_by as author', () => {
329 const src = fs.readFileSync(HOSTED_PROPOSAL_SRC, 'utf8');
330 assert.match(src, /precheckApprovedDelegationProposal\(opts\.dataDir, proposal, \{/);
331 assert.match(src, /author: typeof proposal\.created_by === 'string' \? proposal\.created_by : ''/);
332 });
333
334 test('CONFLICT idempotency unreachable when R2–R5 refuse', async () => {
335 const dir = mkDataDir();
336 const originalFetch = globalThis.fetch;
337 globalThis.fetch = async () => ({
338 ok: true,
339 status: 200,
340 text: async () =>
341 JSON.stringify({
342 proposal_id: 'prop-idem',
343 status: 'approved',
344 intent: 'delegation_consent_create',
345 body: JSON.stringify(
346 makeDelegationConsent({ consentId: 'dcons_idem01', agentId: 'agent_tutor_test01' }),
347 ),
348 frontmatter: JSON.stringify(
349 mergeDelegationFrontmatter({}, {
350 record_kind: 'delegation_consent',
351 consent_id: 'dcons_idem01',
352 }),
353 ),
354 created_by: '',
355 }),
356 });
357 try {
358 enableGate(dir);
359 const identity = makeAgentIdentity();
360 seedDelegationFixtures(dir, 'default', identity);
361 const result = await applyApprovedDelegationProposalFromCanister({
362 dataDir: dir,
363 canisterUrl: 'https://canister.test',
364 headers: {},
365 proposalId: 'prop-idem',
366 });
367 assert.equal(result.ok, false);
368 assert.equal(result.code, 'DELEGATION_AUTHOR_UNVERIFIED');
369 } finally {
370 globalThis.fetch = originalFetch;
371 delete process.env.DELEGATION_ENABLED;
372 fs.rmSync(dir, { recursive: true, force: true });
373 }
374 });
375
376 test('absent/empty principal_ref re-derived before validateConsentRecord', () => {
377 const dir = mkDataDir();
378 try {
379 enableGate(dir);
380 const identity = makeAgentIdentity({ agentId: 'agent_rederive01' });
381 seedDelegationFixtures(dir, 'default', identity);
382 for (const [label, principalRef] of [
383 ['absent', undefined],
384 ['empty', ''],
385 ['non_string', 42],
386 ]) {
387 const body = makeDelegationConsent({
388 consentId: `dcons_rederive_${label}`,
389 agentId: identity.agent_id,
390 });
391 delete body.principal_ref;
392 if (principalRef !== undefined) body.principal_ref = principalRef;
393 const proposal = delegationProposal(dir, body, {
394 record_kind: 'delegation_consent',
395 consent_id: body.consent_id,
396 });
397 const result = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
398 assert.equal(result.ok, true, `case=${label}`);
399 assert.equal(result.record.principal_ref, TEST_PRINCIPAL_REF);
400 }
401 {
402 const body = makeDelegationConsent({
403 consentId: 'dcons_rederive_ws',
404 agentId: identity.agent_id,
405 });
406 body.principal_ref = ' ';
407 const proposal = delegationProposal(dir, body, {
408 record_kind: 'delegation_consent',
409 consent_id: body.consent_id,
410 });
411 const result = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
412 assert.equal(result.ok, true, 'whitespace-only principal_ref');
413 assert.equal(result.record.principal_ref, TEST_PRINCIPAL_REF);
414 }
415 } finally {
416 delete process.env.DELEGATION_ENABLED;
417 fs.rmSync(dir, { recursive: true, force: true });
418 }
419 });
420
421 test('non-empty differing principal_ref refuses even when well-formed', () => {
422 const dir = mkDataDir();
423 try {
424 enableGate(dir);
425 const identity = makeAgentIdentity({ agentId: 'agent_wellformed01' });
426 seedDelegationFixtures(dir, 'default', identity);
427 const body = makeDelegationConsent({
428 consentId: 'dcons_wellformed01',
429 agentId: identity.agent_id,
430 });
431 body.principal_ref = VICTIM_PRINCIPAL;
432 const proposal = delegationProposal(dir, body, {
433 record_kind: 'delegation_consent',
434 consent_id: body.consent_id,
435 });
436 const result = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
437 assert.equal(result.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
438 } finally {
439 delete process.env.DELEGATION_ENABLED;
440 fs.rmSync(dir, { recursive: true, force: true });
441 }
442 });
443
444 test('canister create + GET serializers emit created_by; no userId fallback', () => {
445 const main = fs.readFileSync(MAIN_MO, 'utf8');
446 assert.match(main, /func createdByFromRequest\(req : HttpRequest\) : Text/);
447 assert.match(main, /created_by = createdBy/);
448 assert.match(main, /getHeader\(req, "X-Actor-Id"\)/);
449 assert.doesNotMatch(main, /created_by = userId\(req\)/);
450 assert.match(main, /\\"created_by\\":\\"/);
451 });
452
453 test('Migration.mo pins V5/V6/V7 to ProposalRecordV7; identity hook after SEC-KN-4c (T4)', () => {
454 const migration = fs.readFileSync(MIGRATION_MO, 'utf8');
455 assert.match(migration, /public type ProposalRecordV7/);
456 assert.match(migration, /proposalEntries : \[\(Text, \[ProposalRecordV7\]\)\];/);
457 assert.match(migration, /func _proposalV7ToCurrent\(p : ProposalRecordV7\) : ProposalRecord/);
458 // SEC-KN-4c (T4): actor hook restored to identity on StableStorage; one-shot marker gone.
459 assert.match(
460 migration,
461 /public func migration\(old : \{ var storage : StableStorage \}\) : \{ var storage : StableStorage \}/,
462 );
463 assert.doesNotMatch(migration, /TODO\(SEC-KN-4c\)/);
464 assert.match(
465 migration,
466 /func _proposalBeforeEnrichToCurrent\(p : ProposalRecordBeforeEnrich\) : ProposalRecordV7/,
467 );
468 assert.match(migration, /func _proposalV4ToV5\(p : ProposalRecordV4\) : ProposalRecordV7/);
469 });
470
471 test('npm run canister:verify-migration exits 0', () => {
472 execSync('npm run canister:verify-migration', { cwd: ROOT, stdio: 'pipe' });
473 });
474 });
475
476 // ---------------------------------------------------------------------------
477 // Tier 3 — e2e
478 // ---------------------------------------------------------------------------
479 describe('SEC-KN-4 e2e — honest and hostile apply paths', () => {
480 test('honest path: author A → apply → mint grant principal equals hash(A)', () => {
481 const dir = mkDataDir();
482 try {
483 enableGate(dir);
484 const identity = makeAgentIdentity({ agentId: 'agent_honest01' });
485 seedDelegationFixtures(dir, 'default', identity);
486 const body = makeDelegationConsent({
487 consentId: 'dcons_honest01',
488 agentId: identity.agent_id,
489 });
490 const proposal = createProposal(dir, {
491 path: 'meta/delegation/consents/honest.md',
492 body: JSON.stringify(body),
493 intent: 'delegation_consent_create',
494 source: DELEGATION_PROPOSAL_SOURCE,
495 vault_id: 'default',
496 proposed_by: TEST_USER_ID,
497 delegation_meta: { record_kind: 'delegation_consent', consent_id: body.consent_id },
498 });
499 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
500 assert.equal(pre.ok, true);
501 applyDelegationProposalToIndex(dir, pre);
502 const mint = handleDelegationGrantMintRequest({
503 dataDir: dir,
504 vaultId: 'default',
505 consentId: body.consent_id,
506 actorAgentId: identity.agent_id,
507 });
508 assert.equal(mint.ok, true);
509 assert.equal(mint.payload.grant.principal_ref, TEST_PRINCIPAL_REF);
510 assert.ok(mint.payload.bearer?.startsWith('dgrnt_bearer_'));
511 } finally {
512 delete process.env.DELEGATION_ENABLED;
513 fs.rmSync(dir, { recursive: true, force: true });
514 }
515 });
516
517 test('hostile path: body names victim B, author A → refuse, no consent row, mint unknown', () => {
518 const dir = mkDataDir();
519 try {
520 enableGate(dir);
521 const identity = makeAgentIdentity({ agentId: 'agent_hostile01' });
522 seedDelegationFixtures(dir, 'default', identity);
523 const body = makeDelegationConsent({
524 consentId: 'dcons_hostile01',
525 agentId: identity.agent_id,
526 });
527 body.principal_ref = VICTIM_PRINCIPAL;
528 const proposal = createProposal(dir, {
529 path: 'meta/delegation/consents/hostile.md',
530 body: JSON.stringify(body),
531 intent: 'delegation_consent_create',
532 source: DELEGATION_PROPOSAL_SOURCE,
533 vault_id: 'default',
534 proposed_by: ATTACKER_USER,
535 delegation_meta: { record_kind: 'delegation_consent', consent_id: body.consent_id },
536 });
537 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
538 assert.equal(pre.ok, false);
539 assert.equal(pre.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
540 assert.equal(getConsent(dir, 'default', body.consent_id), null);
541 const mint = handleDelegationGrantMintRequest({
542 dataDir: dir,
543 vaultId: 'default',
544 consentId: body.consent_id,
545 actorAgentId: identity.agent_id,
546 });
547 assert.equal(mint.code, 'unknown_consent');
548 } finally {
549 delete process.env.DELEGATION_ENABLED;
550 fs.rmSync(dir, { recursive: true, force: true });
551 }
552 });
553 });
554
555 // ---------------------------------------------------------------------------
556 // Tier 4 — stress
557 // ---------------------------------------------------------------------------
558 describe('SEC-KN-4 stress — alternating honest/hostile applies', () => {
559 test('200 applies: hostile refuses, honest applies, no store corruption', () => {
560 const dir = mkDataDir();
561 try {
562 enableGate(dir);
563 const identity = makeAgentIdentity({ agentId: 'agent_stress01' });
564 seedDelegationFixtures(dir, 'default', identity);
565 let honestCount = 0;
566 for (let i = 0; i < 200; i += 1) {
567 const hostile = i % 2 === 0;
568 const body = makeDelegationConsent({
569 consentId: `dcons_stress_${String(i).padStart(3, '0')}`,
570 agentId: identity.agent_id,
571 });
572 if (hostile) body.principal_ref = VICTIM_PRINCIPAL;
573 const proposal = delegationProposal(dir, body, {
574 record_kind: 'delegation_consent',
575 consent_id: body.consent_id,
576 });
577 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
578 if (hostile) {
579 assert.equal(pre.ok, false);
580 } else {
581 assert.equal(pre.ok, true);
582 applyDelegationProposalToIndex(dir, pre);
583 honestCount += 1;
584 }
585 }
586 const store = JSON.parse(fs.readFileSync(path.join(dir, DELEGATION_CONSENTS_FILE), 'utf8'));
587 assert.equal(store.vaults.default.consents.length, honestCount);
588 } finally {
589 delete process.env.DELEGATION_ENABLED;
590 fs.rmSync(dir, { recursive: true, force: true });
591 }
592 });
593 });
594
595 // ---------------------------------------------------------------------------
596 // Tier 5 — data-integrity
597 // ---------------------------------------------------------------------------
598 describe('SEC-KN-4 data-integrity — refused applies + idempotent identity', () => {
599 test('refused applies leave consent/identity stores byte-identical', () => {
600 const dir = mkDataDir();
601 try {
602 enableGate(dir);
603 const identity = makeAgentIdentity({ agentId: 'agent_di_refuse01' });
604 seedDelegationFixtures(dir, 'default', identity);
605 const identitiesBefore = fs.readFileSync(path.join(dir, DELEGATION_IDENTITIES_FILE), 'utf8');
606 const consentsPath = path.join(dir, DELEGATION_CONSENTS_FILE);
607 const consentsBefore = fs.existsSync(consentsPath) ? fs.readFileSync(consentsPath, 'utf8') : null;
608 const body = makeDelegationConsent({
609 consentId: 'dcons_di_refuse01',
610 agentId: identity.agent_id,
611 });
612 body.principal_ref = VICTIM_PRINCIPAL;
613 const proposal = delegationProposal(dir, body, {
614 record_kind: 'delegation_consent',
615 consent_id: body.consent_id,
616 });
617 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
618 assert.equal(pre.ok, false);
619 assert.equal(
620 fs.readFileSync(path.join(dir, DELEGATION_IDENTITIES_FILE), 'utf8'),
621 identitiesBefore,
622 );
623 if (consentsBefore === null) {
624 assert.equal(fs.existsSync(consentsPath), false);
625 } else {
626 assert.equal(fs.readFileSync(consentsPath, 'utf8'), consentsBefore);
627 }
628 } finally {
629 delete process.env.DELEGATION_ENABLED;
630 fs.rmSync(dir, { recursive: true, force: true });
631 }
632 });
633
634 test('applied record persists derived principal even when body differed (whitespace-only body)', () => {
635 const dir = mkDataDir();
636 try {
637 enableGate(dir);
638 const identity = makeAgentIdentity({ agentId: 'agent_di_persist01' });
639 seedDelegationFixtures(dir, 'default', identity);
640 const body = makeDelegationConsent({
641 consentId: 'dcons_di_persist01',
642 agentId: identity.agent_id,
643 });
644 body.principal_ref = ' ';
645 const proposal = delegationProposal(dir, body, {
646 record_kind: 'delegation_consent',
647 consent_id: body.consent_id,
648 });
649 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
650 assert.equal(pre.ok, true);
651 applyDelegationProposalToIndex(dir, pre);
652 const stored = getConsent(dir, 'default', body.consent_id);
653 assert.equal(stored.principal_ref, TEST_PRINCIPAL_REF);
654 } finally {
655 delete process.env.DELEGATION_ENABLED;
656 fs.rmSync(dir, { recursive: true, force: true });
657 }
658 });
659
660 test('re-applying agent_identity stays idempotent (CONFLICT)', () => {
661 const dir = mkDataDir();
662 try {
663 enableGate(dir);
664 const body = makeAgentIdentity({ agentId: 'agent_di_idem01' });
665 const proposal = delegationProposal(dir, body, {
666 record_kind: 'agent_identity',
667 agent_id: body.agent_id,
668 });
669 const first = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
670 assert.equal(first.ok, true);
671 applyDelegationProposalToIndex(dir, first);
672 const second = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
673 assert.equal(second.ok, false);
674 assert.equal(second.code, 'CONFLICT');
675 const stored = getAgentIdentity(dir, 'default', body.agent_id);
676 assert.equal(stored.owner_ref, TEST_PRINCIPAL_REF);
677 } finally {
678 delete process.env.DELEGATION_ENABLED;
679 fs.rmSync(dir, { recursive: true, force: true });
680 }
681 });
682 });
683
684 // ---------------------------------------------------------------------------
685 // Tier 6 — performance
686 // ---------------------------------------------------------------------------
687 describe('SEC-KN-4 performance — precheck bounded wall-clock', () => {
688 test('1000 precheck calls complete within generous local budget', () => {
689 const dir = mkDataDir();
690 try {
691 enableGate(dir);
692 const identity = makeAgentIdentity({ agentId: 'agent_perf01' });
693 seedDelegationFixtures(dir, 'default', identity);
694 const body = makeDelegationConsent({
695 consentId: 'dcons_perf01',
696 agentId: identity.agent_id,
697 });
698 const proposal = delegationProposal(dir, body, {
699 record_kind: 'delegation_consent',
700 consent_id: body.consent_id,
701 });
702 const start = performance.now();
703 for (let i = 0; i < 1000; i += 1) {
704 precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
705 }
706 assert.ok(performance.now() - start < 5000, '1000 prechecks should finish within 5s locally');
707 } finally {
708 delete process.env.DELEGATION_ENABLED;
709 fs.rmSync(dir, { recursive: true, force: true });
710 }
711 });
712
713 test('author binding adds no filesystem read beyond the existing store loads', () => {
714 const dir = mkDataDir();
715 const realReadFileSync = fs.readFileSync;
716 try {
717 enableGate(dir);
718 const identity = makeAgentIdentity({ agentId: 'agent_perf02' });
719 seedDelegationFixtures(dir, 'default', identity);
720 const body = makeDelegationConsent({
721 consentId: 'dcons_perf02',
722 agentId: identity.agent_id,
723 });
724 const proposal = delegationProposal(dir, body, {
725 record_kind: 'delegation_consent',
726 consent_id: body.consent_id,
727 });
728
729 // Count reads for a refused call (author fails before any re-derivation work)
730 // and for an accepted one. R2-R5 are pure string/hash operations, so the
731 // accepted path must not read more files than the refusal plus the store loads
732 // the pre-SEC-KN-4 code already performed.
733 const countReads = (author) => {
734 const seen = [];
735 fs.readFileSync = (p, ...rest) => {
736 seen.push(String(p));
737 return realReadFileSync(p, ...rest);
738 };
739 try {
740 precheckApprovedDelegationProposal(dir, proposal, { author });
741 } finally {
742 fs.readFileSync = realReadFileSync;
743 }
744 return seen;
745 };
746
747 const allowed = [
748 DELEGATION_POLICY_FILE,
749 DELEGATION_CONSENTS_FILE,
750 DELEGATION_IDENTITIES_FILE,
751 ];
752 const refusedReads = countReads('');
753 const acceptedReads = countReads(TEST_USER_ID);
754
755 for (const p of [...refusedReads, ...acceptedReads]) {
756 assert.ok(
757 allowed.some((f) => p.includes(f)),
758 `unexpected filesystem read during precheck: ${p}`,
759 );
760 }
761 assert.ok(
762 acceptedReads.length <= allowed.length,
763 `precheck should read at most the gate policy plus the two stores, saw ${acceptedReads.length}`,
764 );
765 assert.ok(
766 refusedReads.length <= acceptedReads.length,
767 'author refusal must not read more than an accepted apply',
768 );
769 } finally {
770 fs.readFileSync = realReadFileSync;
771 delete process.env.DELEGATION_ENABLED;
772 fs.rmSync(dir, { recursive: true, force: true });
773 }
774 });
775 });
776
777 // ---------------------------------------------------------------------------
778 // Tier 7 — security
779 // ---------------------------------------------------------------------------
780 describe('SEC-KN-4 security — regression + anti-regressions', () => {
781 test('legacy body-trusted accepts attacker principal; fixed refuses', () => {
782 const dir = mkDataDir();
783 try {
784 enableGate(dir);
785 const identity = makeAgentIdentity({ agentId: 'agent_sec_reg01' });
786 seedDelegationFixtures(dir, 'default', identity);
787 const body = makeDelegationConsent({
788 consentId: 'dcons_sec_reg01',
789 agentId: identity.agent_id,
790 });
791 body.principal_ref = VICTIM_PRINCIPAL;
792 const proposal = delegationProposal(dir, body, {
793 record_kind: 'delegation_consent',
794 consent_id: body.consent_id,
795 });
796 const legacy = precheckLegacyBodyTrusted(dir, proposal);
797 assert.equal(legacy.ok, true);
798 assert.equal(legacy.record.principal_ref, VICTIM_PRINCIPAL);
799 const fixed = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
800 assert.equal(fixed.ok, false);
801 assert.equal(fixed.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
802 } finally {
803 delete process.env.DELEGATION_ENABLED;
804 fs.rmSync(dir, { recursive: true, force: true });
805 }
806 });
807
808 test('refusal payloads contain no bearer, author, or derived hash', () => {
809 const dir = mkDataDir();
810 try {
811 enableGate(dir);
812 const identity = makeAgentIdentity({ agentId: 'agent_sec_leak01' });
813 seedDelegationFixtures(dir, 'default', identity);
814 const body = makeDelegationConsent({
815 consentId: 'dcons_sec_leak01',
816 agentId: identity.agent_id,
817 });
818 body.principal_ref = VICTIM_PRINCIPAL;
819 const proposal = delegationProposal(dir, body, {
820 record_kind: 'delegation_consent',
821 consent_id: body.consent_id,
822 });
823 const result = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
824 const payload = JSON.stringify(result);
825 assert.equal(payload.includes('dgrnt_bearer_'), false);
826 assert.equal(payload.includes(ATTACKER_USER), false);
827 assert.equal(payload.includes(VICTIM_PRINCIPAL), false);
828 assert.equal(payload.includes(ATTACKER_PRINCIPAL), false);
829 } finally {
830 delete process.env.DELEGATION_ENABLED;
831 fs.rmSync(dir, { recursive: true, force: true });
832 }
833 });
834
835 test('org_ref stored consent cannot mint grant', () => {
836 const dir = mkDataDir();
837 try {
838 enableGate(dir);
839 const identity = makeAgentIdentity({ agentId: 'agent_sec_orgmint01' });
840 const consent = makeDelegationConsent({
841 consentId: 'dcons_sec_orgmint01',
842 agentId: identity.agent_id,
843 });
844 consent.principal_ref = 'org_ref:ws_legacy';
845 seedDelegationFixtures(dir, 'default', identity, consent);
846 const mint = handleDelegationGrantMintRequest({
847 dataDir: dir,
848 vaultId: 'default',
849 consentId: consent.consent_id,
850 actorAgentId: identity.agent_id,
851 });
852 assert.equal(mint.code, 'DELEGATION_CONSENT_PRINCIPAL_INVALID');
853 } finally {
854 delete process.env.DELEGATION_ENABLED;
855 fs.rmSync(dir, { recursive: true, force: true });
856 }
857 });
858
859 test('approver ≠ author: principal stays author-derived (§3.1 anti-regression)', () => {
860 const dir = mkDataDir();
861 try {
862 enableGate(dir);
863 const identity = makeAgentIdentity({ agentId: 'agent_sec_approver01' });
864 seedDelegationFixtures(dir, 'default', identity);
865 const body = makeDelegationConsent({
866 consentId: 'dcons_sec_approver01',
867 agentId: identity.agent_id,
868 });
869 delete body.principal_ref;
870 const proposal = delegationProposal(dir, body, {
871 record_kind: 'delegation_consent',
872 consent_id: body.consent_id,
873 });
874 const memberAuthor = 'member:alice';
875 const ownerApprover = PARTITION_OWNER;
876 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: memberAuthor });
877 assert.equal(pre.ok, true);
878 assert.equal(pre.record.principal_ref, hashPrincipalRef(memberAuthor));
879 assert.notEqual(pre.record.principal_ref, hashPrincipalRef(ownerApprover));
880 } finally {
881 delete process.env.DELEGATION_ENABLED;
882 fs.rmSync(dir, { recursive: true, force: true });
883 }
884 });
885
886 test('R1.5: empty created_by refuses; never binds to partition owner', () => {
887 const dir = mkDataDir();
888 try {
889 enableGate(dir);
890 const identity = makeAgentIdentity({ agentId: 'agent_sec_empty01' });
891 seedDelegationFixtures(dir, 'default', identity);
892 const body = makeDelegationConsent({
893 consentId: 'dcons_sec_empty01',
894 agentId: identity.agent_id,
895 });
896 delete body.principal_ref;
897 const proposal = normalizeCanisterProposalForDelegationPrecheck({
898 proposal_id: 'prop-empty-author',
899 status: 'approved',
900 vault_id: 'default',
901 intent: 'delegation_consent_create',
902 created_by: '',
903 body: JSON.stringify(body),
904 frontmatter: JSON.stringify(
905 mergeDelegationFrontmatter({}, {
906 record_kind: 'delegation_consent',
907 consent_id: body.consent_id,
908 }),
909 ),
910 });
911 assert.ok(proposal);
912 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: '' });
913 assert.equal(pre.code, 'DELEGATION_AUTHOR_UNVERIFIED');
914 assert.equal(pre.ok, false);
915
916 // The refusal must not have bound the consent to the partition owner. Both the
917 // returned record and the persisted store are checked: a fallback to the owner
918 // would surface as the owner's derived principal in one of them.
919 const ownerDerived = hashPrincipalRef(PARTITION_OWNER);
920 assert.equal(pre.record, undefined);
921 const consentsPath = path.join(dir, DELEGATION_CONSENTS_FILE);
922 const persisted = fs.existsSync(consentsPath)
923 ? fs.readFileSync(consentsPath, 'utf8')
924 : '';
925 assert.ok(!persisted.includes(ownerDerived), 'owner principal must never be persisted');
926 assert.ok(!persisted.includes(body.consent_id), 'refused consent must not be stored');
927 } finally {
928 delete process.env.DELEGATION_ENABLED;
929 fs.rmSync(dir, { recursive: true, force: true });
930 }
931 });
932
933 test('R8: bridge apply uses effectiveCanisterUid partition (no cross-partition fetch)', () => {
934 const src = fs.readFileSync(DELEGATION_ROUTES, 'utf8');
935 assert.match(src, /'X-User-Id': hctx\.effectiveCanisterUid/);
936 assert.match(src, /applyApprovedDelegationProposalFromCanister/);
937 });
938
939 test('R9: delegation intents absent from personal self-apply fingerprint class', () => {
940 assert.equal(
941 matchesScoolingReviewTrayFingerprint({ intent: 'delegation_consent_create', path: 'x', external_ref: 'y' }),
942 false,
943 );
944 assert.equal(
945 matchesScoolingReviewTrayFingerprint({ intent: 'agent_identity_register', path: 'x', external_ref: 'y' }),
946 false,
947 );
948 });
949
950 test('R6: identity propose always derives owner_ref from session userId', async () => {
951 const dir = mkDataDir();
952 try {
953 enableGate(dir);
954 const created = [];
955 const result = await handleAgentIdentityRegisterProposeRequest({
956 dataDir: dir,
957 vaultId: 'default',
958 userId: TEST_USER_ID,
959 kind: 'delegate',
960 agentId: 'agent_r6_test01',
961 createProposal: (_dataDir, input) => {
962 created.push(input);
963 return { proposal_id: 'prop-r6' };
964 },
965 });
966 assert.equal(result.ok, true);
967 const body = JSON.parse(created[0].body);
968 assert.equal(body.owner_ref, TEST_PRINCIPAL_REF);
969 } finally {
970 delete process.env.DELEGATION_ENABLED;
971 fs.rmSync(dir, { recursive: true, force: true });
972 }
973 });
974 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago