sec-kn-4-delegation-principal-binding.test.mjs
969 lines 37.6 KB
Raw
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 43 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 and _proposalV7ToCurrent + TODO(SEC-KN-4c)', () => {
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 assert.match(migration, /TODO\(SEC-KN-4c\)/);
459 assert.match(
460 migration,
461 /func _proposalBeforeEnrichToCurrent\(p : ProposalRecordBeforeEnrich\) : ProposalRecordV7/,
462 );
463 assert.match(migration, /func _proposalV4ToV5\(p : ProposalRecordV4\) : ProposalRecordV7/);
464 });
465
466 test('npm run canister:verify-migration exits 0', () => {
467 execSync('npm run canister:verify-migration', { cwd: ROOT, stdio: 'pipe' });
468 });
469 });
470
471 // ---------------------------------------------------------------------------
472 // Tier 3 — e2e
473 // ---------------------------------------------------------------------------
474 describe('SEC-KN-4 e2e — honest and hostile apply paths', () => {
475 test('honest path: author A → apply → mint grant principal equals hash(A)', () => {
476 const dir = mkDataDir();
477 try {
478 enableGate(dir);
479 const identity = makeAgentIdentity({ agentId: 'agent_honest01' });
480 seedDelegationFixtures(dir, 'default', identity);
481 const body = makeDelegationConsent({
482 consentId: 'dcons_honest01',
483 agentId: identity.agent_id,
484 });
485 const proposal = createProposal(dir, {
486 path: 'meta/delegation/consents/honest.md',
487 body: JSON.stringify(body),
488 intent: 'delegation_consent_create',
489 source: DELEGATION_PROPOSAL_SOURCE,
490 vault_id: 'default',
491 proposed_by: TEST_USER_ID,
492 delegation_meta: { record_kind: 'delegation_consent', consent_id: body.consent_id },
493 });
494 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
495 assert.equal(pre.ok, true);
496 applyDelegationProposalToIndex(dir, pre);
497 const mint = handleDelegationGrantMintRequest({
498 dataDir: dir,
499 vaultId: 'default',
500 consentId: body.consent_id,
501 actorAgentId: identity.agent_id,
502 });
503 assert.equal(mint.ok, true);
504 assert.equal(mint.payload.grant.principal_ref, TEST_PRINCIPAL_REF);
505 assert.ok(mint.payload.bearer?.startsWith('dgrnt_bearer_'));
506 } finally {
507 delete process.env.DELEGATION_ENABLED;
508 fs.rmSync(dir, { recursive: true, force: true });
509 }
510 });
511
512 test('hostile path: body names victim B, author A → refuse, no consent row, mint unknown', () => {
513 const dir = mkDataDir();
514 try {
515 enableGate(dir);
516 const identity = makeAgentIdentity({ agentId: 'agent_hostile01' });
517 seedDelegationFixtures(dir, 'default', identity);
518 const body = makeDelegationConsent({
519 consentId: 'dcons_hostile01',
520 agentId: identity.agent_id,
521 });
522 body.principal_ref = VICTIM_PRINCIPAL;
523 const proposal = createProposal(dir, {
524 path: 'meta/delegation/consents/hostile.md',
525 body: JSON.stringify(body),
526 intent: 'delegation_consent_create',
527 source: DELEGATION_PROPOSAL_SOURCE,
528 vault_id: 'default',
529 proposed_by: ATTACKER_USER,
530 delegation_meta: { record_kind: 'delegation_consent', consent_id: body.consent_id },
531 });
532 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
533 assert.equal(pre.ok, false);
534 assert.equal(pre.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
535 assert.equal(getConsent(dir, 'default', body.consent_id), null);
536 const mint = handleDelegationGrantMintRequest({
537 dataDir: dir,
538 vaultId: 'default',
539 consentId: body.consent_id,
540 actorAgentId: identity.agent_id,
541 });
542 assert.equal(mint.code, 'unknown_consent');
543 } finally {
544 delete process.env.DELEGATION_ENABLED;
545 fs.rmSync(dir, { recursive: true, force: true });
546 }
547 });
548 });
549
550 // ---------------------------------------------------------------------------
551 // Tier 4 — stress
552 // ---------------------------------------------------------------------------
553 describe('SEC-KN-4 stress — alternating honest/hostile applies', () => {
554 test('200 applies: hostile refuses, honest applies, no store corruption', () => {
555 const dir = mkDataDir();
556 try {
557 enableGate(dir);
558 const identity = makeAgentIdentity({ agentId: 'agent_stress01' });
559 seedDelegationFixtures(dir, 'default', identity);
560 let honestCount = 0;
561 for (let i = 0; i < 200; i += 1) {
562 const hostile = i % 2 === 0;
563 const body = makeDelegationConsent({
564 consentId: `dcons_stress_${String(i).padStart(3, '0')}`,
565 agentId: identity.agent_id,
566 });
567 if (hostile) body.principal_ref = VICTIM_PRINCIPAL;
568 const proposal = delegationProposal(dir, body, {
569 record_kind: 'delegation_consent',
570 consent_id: body.consent_id,
571 });
572 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
573 if (hostile) {
574 assert.equal(pre.ok, false);
575 } else {
576 assert.equal(pre.ok, true);
577 applyDelegationProposalToIndex(dir, pre);
578 honestCount += 1;
579 }
580 }
581 const store = JSON.parse(fs.readFileSync(path.join(dir, DELEGATION_CONSENTS_FILE), 'utf8'));
582 assert.equal(store.vaults.default.consents.length, honestCount);
583 } finally {
584 delete process.env.DELEGATION_ENABLED;
585 fs.rmSync(dir, { recursive: true, force: true });
586 }
587 });
588 });
589
590 // ---------------------------------------------------------------------------
591 // Tier 5 — data-integrity
592 // ---------------------------------------------------------------------------
593 describe('SEC-KN-4 data-integrity — refused applies + idempotent identity', () => {
594 test('refused applies leave consent/identity stores byte-identical', () => {
595 const dir = mkDataDir();
596 try {
597 enableGate(dir);
598 const identity = makeAgentIdentity({ agentId: 'agent_di_refuse01' });
599 seedDelegationFixtures(dir, 'default', identity);
600 const identitiesBefore = fs.readFileSync(path.join(dir, DELEGATION_IDENTITIES_FILE), 'utf8');
601 const consentsPath = path.join(dir, DELEGATION_CONSENTS_FILE);
602 const consentsBefore = fs.existsSync(consentsPath) ? fs.readFileSync(consentsPath, 'utf8') : null;
603 const body = makeDelegationConsent({
604 consentId: 'dcons_di_refuse01',
605 agentId: identity.agent_id,
606 });
607 body.principal_ref = VICTIM_PRINCIPAL;
608 const proposal = delegationProposal(dir, body, {
609 record_kind: 'delegation_consent',
610 consent_id: body.consent_id,
611 });
612 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
613 assert.equal(pre.ok, false);
614 assert.equal(
615 fs.readFileSync(path.join(dir, DELEGATION_IDENTITIES_FILE), 'utf8'),
616 identitiesBefore,
617 );
618 if (consentsBefore === null) {
619 assert.equal(fs.existsSync(consentsPath), false);
620 } else {
621 assert.equal(fs.readFileSync(consentsPath, 'utf8'), consentsBefore);
622 }
623 } finally {
624 delete process.env.DELEGATION_ENABLED;
625 fs.rmSync(dir, { recursive: true, force: true });
626 }
627 });
628
629 test('applied record persists derived principal even when body differed (whitespace-only body)', () => {
630 const dir = mkDataDir();
631 try {
632 enableGate(dir);
633 const identity = makeAgentIdentity({ agentId: 'agent_di_persist01' });
634 seedDelegationFixtures(dir, 'default', identity);
635 const body = makeDelegationConsent({
636 consentId: 'dcons_di_persist01',
637 agentId: identity.agent_id,
638 });
639 body.principal_ref = ' ';
640 const proposal = delegationProposal(dir, body, {
641 record_kind: 'delegation_consent',
642 consent_id: body.consent_id,
643 });
644 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
645 assert.equal(pre.ok, true);
646 applyDelegationProposalToIndex(dir, pre);
647 const stored = getConsent(dir, 'default', body.consent_id);
648 assert.equal(stored.principal_ref, TEST_PRINCIPAL_REF);
649 } finally {
650 delete process.env.DELEGATION_ENABLED;
651 fs.rmSync(dir, { recursive: true, force: true });
652 }
653 });
654
655 test('re-applying agent_identity stays idempotent (CONFLICT)', () => {
656 const dir = mkDataDir();
657 try {
658 enableGate(dir);
659 const body = makeAgentIdentity({ agentId: 'agent_di_idem01' });
660 const proposal = delegationProposal(dir, body, {
661 record_kind: 'agent_identity',
662 agent_id: body.agent_id,
663 });
664 const first = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
665 assert.equal(first.ok, true);
666 applyDelegationProposalToIndex(dir, first);
667 const second = precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
668 assert.equal(second.ok, false);
669 assert.equal(second.code, 'CONFLICT');
670 const stored = getAgentIdentity(dir, 'default', body.agent_id);
671 assert.equal(stored.owner_ref, TEST_PRINCIPAL_REF);
672 } finally {
673 delete process.env.DELEGATION_ENABLED;
674 fs.rmSync(dir, { recursive: true, force: true });
675 }
676 });
677 });
678
679 // ---------------------------------------------------------------------------
680 // Tier 6 — performance
681 // ---------------------------------------------------------------------------
682 describe('SEC-KN-4 performance — precheck bounded wall-clock', () => {
683 test('1000 precheck calls complete within generous local budget', () => {
684 const dir = mkDataDir();
685 try {
686 enableGate(dir);
687 const identity = makeAgentIdentity({ agentId: 'agent_perf01' });
688 seedDelegationFixtures(dir, 'default', identity);
689 const body = makeDelegationConsent({
690 consentId: 'dcons_perf01',
691 agentId: identity.agent_id,
692 });
693 const proposal = delegationProposal(dir, body, {
694 record_kind: 'delegation_consent',
695 consent_id: body.consent_id,
696 });
697 const start = performance.now();
698 for (let i = 0; i < 1000; i += 1) {
699 precheckApprovedDelegationProposal(dir, proposal, { author: TEST_USER_ID });
700 }
701 assert.ok(performance.now() - start < 5000, '1000 prechecks should finish within 5s locally');
702 } finally {
703 delete process.env.DELEGATION_ENABLED;
704 fs.rmSync(dir, { recursive: true, force: true });
705 }
706 });
707
708 test('author binding adds no filesystem read beyond the existing store loads', () => {
709 const dir = mkDataDir();
710 const realReadFileSync = fs.readFileSync;
711 try {
712 enableGate(dir);
713 const identity = makeAgentIdentity({ agentId: 'agent_perf02' });
714 seedDelegationFixtures(dir, 'default', identity);
715 const body = makeDelegationConsent({
716 consentId: 'dcons_perf02',
717 agentId: identity.agent_id,
718 });
719 const proposal = delegationProposal(dir, body, {
720 record_kind: 'delegation_consent',
721 consent_id: body.consent_id,
722 });
723
724 // Count reads for a refused call (author fails before any re-derivation work)
725 // and for an accepted one. R2-R5 are pure string/hash operations, so the
726 // accepted path must not read more files than the refusal plus the store loads
727 // the pre-SEC-KN-4 code already performed.
728 const countReads = (author) => {
729 const seen = [];
730 fs.readFileSync = (p, ...rest) => {
731 seen.push(String(p));
732 return realReadFileSync(p, ...rest);
733 };
734 try {
735 precheckApprovedDelegationProposal(dir, proposal, { author });
736 } finally {
737 fs.readFileSync = realReadFileSync;
738 }
739 return seen;
740 };
741
742 const allowed = [
743 DELEGATION_POLICY_FILE,
744 DELEGATION_CONSENTS_FILE,
745 DELEGATION_IDENTITIES_FILE,
746 ];
747 const refusedReads = countReads('');
748 const acceptedReads = countReads(TEST_USER_ID);
749
750 for (const p of [...refusedReads, ...acceptedReads]) {
751 assert.ok(
752 allowed.some((f) => p.includes(f)),
753 `unexpected filesystem read during precheck: ${p}`,
754 );
755 }
756 assert.ok(
757 acceptedReads.length <= allowed.length,
758 `precheck should read at most the gate policy plus the two stores, saw ${acceptedReads.length}`,
759 );
760 assert.ok(
761 refusedReads.length <= acceptedReads.length,
762 'author refusal must not read more than an accepted apply',
763 );
764 } finally {
765 fs.readFileSync = realReadFileSync;
766 delete process.env.DELEGATION_ENABLED;
767 fs.rmSync(dir, { recursive: true, force: true });
768 }
769 });
770 });
771
772 // ---------------------------------------------------------------------------
773 // Tier 7 — security
774 // ---------------------------------------------------------------------------
775 describe('SEC-KN-4 security — regression + anti-regressions', () => {
776 test('legacy body-trusted accepts attacker principal; fixed refuses', () => {
777 const dir = mkDataDir();
778 try {
779 enableGate(dir);
780 const identity = makeAgentIdentity({ agentId: 'agent_sec_reg01' });
781 seedDelegationFixtures(dir, 'default', identity);
782 const body = makeDelegationConsent({
783 consentId: 'dcons_sec_reg01',
784 agentId: identity.agent_id,
785 });
786 body.principal_ref = VICTIM_PRINCIPAL;
787 const proposal = delegationProposal(dir, body, {
788 record_kind: 'delegation_consent',
789 consent_id: body.consent_id,
790 });
791 const legacy = precheckLegacyBodyTrusted(dir, proposal);
792 assert.equal(legacy.ok, true);
793 assert.equal(legacy.record.principal_ref, VICTIM_PRINCIPAL);
794 const fixed = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
795 assert.equal(fixed.ok, false);
796 assert.equal(fixed.code, 'DELEGATION_PRINCIPAL_REBIND_MISMATCH');
797 } finally {
798 delete process.env.DELEGATION_ENABLED;
799 fs.rmSync(dir, { recursive: true, force: true });
800 }
801 });
802
803 test('refusal payloads contain no bearer, author, or derived hash', () => {
804 const dir = mkDataDir();
805 try {
806 enableGate(dir);
807 const identity = makeAgentIdentity({ agentId: 'agent_sec_leak01' });
808 seedDelegationFixtures(dir, 'default', identity);
809 const body = makeDelegationConsent({
810 consentId: 'dcons_sec_leak01',
811 agentId: identity.agent_id,
812 });
813 body.principal_ref = VICTIM_PRINCIPAL;
814 const proposal = delegationProposal(dir, body, {
815 record_kind: 'delegation_consent',
816 consent_id: body.consent_id,
817 });
818 const result = precheckApprovedDelegationProposal(dir, proposal, { author: ATTACKER_USER });
819 const payload = JSON.stringify(result);
820 assert.equal(payload.includes('dgrnt_bearer_'), false);
821 assert.equal(payload.includes(ATTACKER_USER), false);
822 assert.equal(payload.includes(VICTIM_PRINCIPAL), false);
823 assert.equal(payload.includes(ATTACKER_PRINCIPAL), false);
824 } finally {
825 delete process.env.DELEGATION_ENABLED;
826 fs.rmSync(dir, { recursive: true, force: true });
827 }
828 });
829
830 test('org_ref stored consent cannot mint grant', () => {
831 const dir = mkDataDir();
832 try {
833 enableGate(dir);
834 const identity = makeAgentIdentity({ agentId: 'agent_sec_orgmint01' });
835 const consent = makeDelegationConsent({
836 consentId: 'dcons_sec_orgmint01',
837 agentId: identity.agent_id,
838 });
839 consent.principal_ref = 'org_ref:ws_legacy';
840 seedDelegationFixtures(dir, 'default', identity, consent);
841 const mint = handleDelegationGrantMintRequest({
842 dataDir: dir,
843 vaultId: 'default',
844 consentId: consent.consent_id,
845 actorAgentId: identity.agent_id,
846 });
847 assert.equal(mint.code, 'DELEGATION_CONSENT_PRINCIPAL_INVALID');
848 } finally {
849 delete process.env.DELEGATION_ENABLED;
850 fs.rmSync(dir, { recursive: true, force: true });
851 }
852 });
853
854 test('approver ≠ author: principal stays author-derived (§3.1 anti-regression)', () => {
855 const dir = mkDataDir();
856 try {
857 enableGate(dir);
858 const identity = makeAgentIdentity({ agentId: 'agent_sec_approver01' });
859 seedDelegationFixtures(dir, 'default', identity);
860 const body = makeDelegationConsent({
861 consentId: 'dcons_sec_approver01',
862 agentId: identity.agent_id,
863 });
864 delete body.principal_ref;
865 const proposal = delegationProposal(dir, body, {
866 record_kind: 'delegation_consent',
867 consent_id: body.consent_id,
868 });
869 const memberAuthor = 'member:alice';
870 const ownerApprover = PARTITION_OWNER;
871 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: memberAuthor });
872 assert.equal(pre.ok, true);
873 assert.equal(pre.record.principal_ref, hashPrincipalRef(memberAuthor));
874 assert.notEqual(pre.record.principal_ref, hashPrincipalRef(ownerApprover));
875 } finally {
876 delete process.env.DELEGATION_ENABLED;
877 fs.rmSync(dir, { recursive: true, force: true });
878 }
879 });
880
881 test('R1.5: empty created_by refuses; never binds to partition owner', () => {
882 const dir = mkDataDir();
883 try {
884 enableGate(dir);
885 const identity = makeAgentIdentity({ agentId: 'agent_sec_empty01' });
886 seedDelegationFixtures(dir, 'default', identity);
887 const body = makeDelegationConsent({
888 consentId: 'dcons_sec_empty01',
889 agentId: identity.agent_id,
890 });
891 delete body.principal_ref;
892 const proposal = normalizeCanisterProposalForDelegationPrecheck({
893 proposal_id: 'prop-empty-author',
894 status: 'approved',
895 vault_id: 'default',
896 intent: 'delegation_consent_create',
897 created_by: '',
898 body: JSON.stringify(body),
899 frontmatter: JSON.stringify(
900 mergeDelegationFrontmatter({}, {
901 record_kind: 'delegation_consent',
902 consent_id: body.consent_id,
903 }),
904 ),
905 });
906 assert.ok(proposal);
907 const pre = precheckApprovedDelegationProposal(dir, proposal, { author: '' });
908 assert.equal(pre.code, 'DELEGATION_AUTHOR_UNVERIFIED');
909 assert.equal(pre.ok, false);
910
911 // The refusal must not have bound the consent to the partition owner. Both the
912 // returned record and the persisted store are checked: a fallback to the owner
913 // would surface as the owner's derived principal in one of them.
914 const ownerDerived = hashPrincipalRef(PARTITION_OWNER);
915 assert.equal(pre.record, undefined);
916 const consentsPath = path.join(dir, DELEGATION_CONSENTS_FILE);
917 const persisted = fs.existsSync(consentsPath)
918 ? fs.readFileSync(consentsPath, 'utf8')
919 : '';
920 assert.ok(!persisted.includes(ownerDerived), 'owner principal must never be persisted');
921 assert.ok(!persisted.includes(body.consent_id), 'refused consent must not be stored');
922 } finally {
923 delete process.env.DELEGATION_ENABLED;
924 fs.rmSync(dir, { recursive: true, force: true });
925 }
926 });
927
928 test('R8: bridge apply uses effectiveCanisterUid partition (no cross-partition fetch)', () => {
929 const src = fs.readFileSync(DELEGATION_ROUTES, 'utf8');
930 assert.match(src, /'X-User-Id': hctx\.effectiveCanisterUid/);
931 assert.match(src, /applyApprovedDelegationProposalFromCanister/);
932 });
933
934 test('R9: delegation intents absent from personal self-apply fingerprint class', () => {
935 assert.equal(
936 matchesScoolingReviewTrayFingerprint({ intent: 'delegation_consent_create', path: 'x', external_ref: 'y' }),
937 false,
938 );
939 assert.equal(
940 matchesScoolingReviewTrayFingerprint({ intent: 'agent_identity_register', path: 'x', external_ref: 'y' }),
941 false,
942 );
943 });
944
945 test('R6: identity propose always derives owner_ref from session userId', async () => {
946 const dir = mkDataDir();
947 try {
948 enableGate(dir);
949 const created = [];
950 const result = await handleAgentIdentityRegisterProposeRequest({
951 dataDir: dir,
952 vaultId: 'default',
953 userId: TEST_USER_ID,
954 kind: 'delegate',
955 agentId: 'agent_r6_test01',
956 createProposal: (_dataDir, input) => {
957 created.push(input);
958 return { proposal_id: 'prop-r6' };
959 },
960 });
961 assert.equal(result.ok, true);
962 const body = JSON.parse(created[0].body);
963 assert.equal(body.owner_ref, TEST_PRINCIPAL_REF);
964 } finally {
965 delete process.env.DELEGATION_ENABLED;
966 fs.rmSync(dir, { recursive: true, force: true });
967 }
968 });
969 });
File History 1 commit
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 43 days ago