@shieldaai/agent-guard-scenarios 0.1.0-alpha.0 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,20 @@ console.table(report.results);
11
11
  console.log(scenarioCatalog);
12
12
  ```
13
13
 
14
- Run selected scenarios with `runConformance({ scenarioIds: ['S05', 'S08'] })`. The default suite executes malicious and benign variants of all ten scenarios. It checks handler invocations and synthetic effects, not merely decision objects. Fixtures do not use real credentials, models, network services or destructive operations.
14
+ Run selected scenarios with `runConformance({ scenarioIds: ['S05', 'S08'] })`. The default suite executes malicious and benign variants of all twelve scenarios. It checks handler invocations and synthetic effects, not merely decision objects. The built-in fixtures use no credentials, model calls, network services or destructive operations. A custom adapter is responsible for its own I/O.
15
15
 
16
- The scenarios cover unauthorized tools/resources, delegation, protected administration, sensitive export, action-bound approval, metadata drift, outbound secrets/canaries, revocation/control failures, tenants and budgets. The [scenario specification](https://github.com/ShieldaAI/agent-guard/blob/main/docs/scenarios.md) explains each boundary.
16
+ The scenarios cover unauthorized tools/resources, delegation, protected administration, sensitive export, action-bound approval, metadata drift, outbound secrets/canaries, revocation/control failures, tenants, budgets, normalized typed arguments and resource-version checks. The [scenario specification](https://github.com/ShieldaAI/agent-guard/blob/main/docs/scenarios.md) explains each boundary.
17
+
18
+ The catalog version identifies the executed acceptance checks independently of the package version; compare reports using their `catalogVersion`. Version `1.2.0` retains S01–S10 and adds S11 and S12, for twelve scenario identifiers and twenty-four paired cases. A pass from an older catalog does not establish coverage of newly added assertions.
19
+
20
+ S11 checks that the trusted `ToolDefinition.parse` rejects wrong types, unknown or missing fields, invalid ranges and invalid or asynchronous parser results before resource resolution. The benign case verifies that normalized arguments reach resolution, both policy evaluations, approval and execution; later mutations of caller or parser-owned objects cannot change the approved effect. The core does not interpret `inputSchema` automatically: the host must supply a suitable synchronous parser.
21
+
22
+ S12 binds the resolver's `resourceVersion` to the immutable prepared action and approval. A synthetic trusted handler compares the approved version with current state immediately before its write. Version forgery and approval transfer must fail before dispatch; a genuine stale version enters the handler, consumes its attempt and produces no write. The benign pair approves and writes two successive versions separately. Real services must implement the comparison atomically with their side effect. This scenario does not claim the core locks or refreshes external resources.
17
23
 
18
24
  To exercise a compatible implementation, supply `createGuard(options)` in the options object. The factory must honor the provided clock and secret configuration and return the structural `GuardLike` API. Denials use the exported `GuardError` and its documented reason codes. Each fixture receives an isolated instance; no shared factory state is required.
19
25
 
26
+ Each malicious or benign variant has a five-second asynchronous timeout. Override it with `caseTimeoutMs`, a positive integer up to `2147483647`. A timed-out variant receives a failed result and the runner continues with the next variant. The timer cannot interrupt blocking JavaScript or cancel unfinished adapter work; an adapter may retain resources or continue running after its result is reported. Use process isolation if the implementation under test needs a hard execution limit.
27
+
20
28
  A passing report applies to these fixtures in one process. It is not a security certification, injection-detector benchmark, MCP transport test or proof that a production agent cannot bypass its integration. Errors after actual handler invocation are tested separately from denials that must prevent invocation.
21
29
 
22
30
  Apache-2.0. Copyright 2026 ShieldaAI contributors.
package/dist/catalog.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Version of the synthetic acceptance cases, independent of package releases. */
2
- export declare const SCENARIO_CATALOG_VERSION: '1.0.0';
3
- export type ScenarioId = 'S01' | 'S02' | 'S03' | 'S04' | 'S05' | 'S06' | 'S07' | 'S08' | 'S09' | 'S10';
2
+ export declare const SCENARIO_CATALOG_VERSION: '1.2.0';
3
+ export type ScenarioId = 'S01' | 'S02' | 'S03' | 'S04' | 'S05' | 'S06' | 'S07' | 'S08' | 'S09' | 'S10' | 'S11' | 'S12';
4
4
  export interface ScenarioDefinition {
5
5
  readonly id: ScenarioId;
6
6
  readonly title: string;
@@ -9,5 +9,5 @@ export interface ScenarioDefinition {
9
9
  readonly benign: string;
10
10
  readonly limitations: readonly string[];
11
11
  }
12
- /** Immutable descriptive catalog. Execution is opt-in through runConformance(). */
12
+ /** Read-only scenario descriptions. Call runConformance() to run the fixtures. */
13
13
  export declare const scenarioCatalog: readonly ScenarioDefinition[];
package/dist/catalog.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Version of the synthetic acceptance cases, independent of package releases. */
2
- export const SCENARIO_CATALOG_VERSION = '1.0.0';
2
+ export const SCENARIO_CATALOG_VERSION = '1.2.0';
3
3
  const definitions = [
4
4
  {
5
5
  id: 'S01', title: 'Deny unknown tools and unauthorized resources',
@@ -71,8 +71,22 @@ const definitions = [
71
71
  benign: 'Authorized work releases concurrency, reconciles cost and cooperates with cancellation.',
72
72
  limitations: ['Quotas are local and nondurable; the provider must enforce declared upper costs and handlers must honor cancellation.'],
73
73
  },
74
+ {
75
+ id: 'S11', title: 'Validate and bind normalized typed arguments',
76
+ boundary: 'A trusted synchronous parser runs before resolution; its immutable normalized output reaches policy, approval and the handler.',
77
+ malicious: ['Wrong field types', 'Unknown fields', 'Missing required fields', 'Out-of-range values', 'Invalid parser output', 'Asynchronous parser'],
78
+ benign: 'One approved write uses the normalized arguments even after caller and parser-owned objects change.',
79
+ limitations: ['The host supplies the parser; descriptive inputSchema metadata alone does not validate core arguments.'],
80
+ },
81
+ {
82
+ id: 'S12', title: 'Bind approvals to resource versions and compare at use time',
83
+ boundary: 'The immutable action includes the resolved version; a trusted handler atomically compares it before applying the effect.',
84
+ malicious: ['Forged prepared resource version', 'Approval transferred to a new version', 'Resource changed after approval', 'Replay of a stale admitted attempt'],
85
+ benign: 'An unchanged approved version is written once; a subsequent version requires a fresh preparation and approval.',
86
+ limitations: ['The core does not fetch or lock external state. A stale attempt enters the handler, which must enforce the comparison atomically with the real effect.'],
87
+ },
74
88
  ];
75
- /** Immutable descriptive catalog. Execution is opt-in through runConformance(). */
89
+ /** Read-only scenario descriptions. Call runConformance() to run the fixtures. */
76
90
  export const scenarioCatalog = Object.freeze(definitions.map((entry) => Object.freeze({
77
91
  ...entry,
78
92
  malicious: Object.freeze([...entry.malicious]),
package/dist/index.d.ts CHANGED
@@ -2,14 +2,16 @@ import { AgentGuard, type GuardOptions } from '@shieldaai/agent-guard';
2
2
  import { SCENARIO_CATALOG_VERSION, type ScenarioId } from './catalog.js';
3
3
  export { SCENARIO_CATALOG_VERSION, scenarioCatalog } from './catalog.js';
4
4
  export type { ScenarioDefinition, ScenarioId } from './catalog.js';
5
- /** Structural surface required from a compatible guard adapter. */
5
+ /** Methods an adapter must provide to run the conformance suite. */
6
6
  export type GuardLike = Pick<AgentGuard, 'registerTool' | 'approveTool' | 'createTask' | 'delegateTask' | 'revokeTask' | 'setPolicy' | 'prepare' | 'approve' | 'execute' | 'snapshot'>;
7
7
  export type ConformanceGuardFactory = (options: GuardOptions) => GuardLike;
8
8
  export interface ConformanceOptions {
9
- /** Omit to execute all ten scenarios. Each selected scenario runs both variants. */
9
+ /** Omit to execute all twelve scenarios. Each selected scenario runs both variants. */
10
10
  readonly scenarioIds?: readonly ScenarioId[];
11
+ /** Time limit per variant in milliseconds; defaults to 5000. Timeout does not cancel adapter work. */
12
+ readonly caseTimeoutMs?: number;
11
13
  /**
12
- * Supply a fresh isolated guard per call; defaults to the public AgentGuard implementation.
14
+ * Return a new guard for each call. Defaults to the public AgentGuard implementation.
13
15
  * Compatible adapters must preserve these APIs and throw this core package's GuardError
14
16
  * with the documented reason codes. The provided clock and secret configuration must be honored.
15
17
  */
@@ -35,8 +37,8 @@ export interface ConformanceReport {
35
37
  readonly results: readonly ConformanceCaseResult[];
36
38
  }
37
39
  /**
38
- * Run synthetic effects against this installation's public core API.
39
- * This validates the listed local fixtures, not a deployment, transport, or universal injection defense.
40
- * No network, credentials, filesystem writes, model calls, or external effects are used.
40
+ * Run the catalog fixtures against AgentGuard or the supplied adapter.
41
+ * The default implementation uses in-memory effects only. A custom adapter is
42
+ * responsible for its own I/O; the runner cannot isolate or cancel it.
41
43
  */
42
44
  export declare function runConformance(options?: ConformanceOptions): Promise<ConformanceReport>;
package/dist/index.js CHANGED
@@ -28,11 +28,11 @@ function deferred() {
28
28
  const promise = new Promise((done) => { resolve = done; });
29
29
  return { promise, resolve };
30
30
  }
31
- async function bounded(promise, label) {
31
+ async function bounded(promise, label, timeoutMs = 1500) {
32
32
  let timer;
33
33
  try {
34
34
  return await Promise.race([promise, new Promise((_, reject) => {
35
- timer = setTimeout(() => reject(new CheckFailure(label)), 1500);
35
+ timer = setTimeout(() => reject(new CheckFailure(label)), timeoutMs);
36
36
  })]);
37
37
  }
38
38
  finally {
@@ -341,20 +341,23 @@ const cases = {
341
341
  const prepared = await f.guard.prepare(task, { tool: 'delete_record', args: { resource: RECORD_A } });
342
342
  const approvalId = f.approve(prepared);
343
343
  wait = true;
344
- const pending = f.guard.execute(prepared, { approvalId });
345
- // Observe early settlement immediately, including when testing a broken implementation.
346
- const observed = pending.then(() => undefined, () => undefined);
347
- try {
348
- await bounded(Promise.race([entered.promise, observed.then(() => { throw new CheckFailure('execution settled before policy barrier'); })]), 'policy barrier not reached');
349
- if (change === 'revoke')
350
- f.guard.revokeTask(task);
351
- else
352
- f.guard.setPolicy(() => 'allow');
353
- }
354
- finally {
355
- release.resolve();
356
- }
357
- await c.denied(() => pending, `${change} during asynchronous policy`, change === 'revoke' ? 'TASK_REVOKED' : 'POLICY_CHANGED');
344
+ await c.denied(async () => {
345
+ const pending = f.guard.execute(prepared, { approvalId });
346
+ // Observe early settlement immediately, including when testing a broken implementation.
347
+ const observed = pending.then(() => undefined, () => undefined);
348
+ try {
349
+ await bounded(Promise.race([entered.promise, observed.then(() => { throw new CheckFailure('execution settled before policy barrier'); })]), 'policy barrier not reached');
350
+ if (change === 'revoke')
351
+ f.guard.revokeTask(task);
352
+ else
353
+ f.guard.setPolicy(() => 'allow');
354
+ }
355
+ finally {
356
+ release.resolve();
357
+ }
358
+ return pending;
359
+ }, `${change} during asynchronous policy`, change === 'revoke' ? 'TASK_REVOKED' : 'POLICY_CHANGED');
360
+ c.assert(f.ledger.length === 0, `${change} during asynchronous policy leaves the ledger empty`);
358
361
  }
359
362
  const f = fixture(c);
360
363
  f.remove();
@@ -485,6 +488,13 @@ const cases = {
485
488
  await f.invoke(task, 'read_document', { resource: GUIDE });
486
489
  const snapshot = f.guard.snapshot(task);
487
490
  c.assert(snapshot.calls === 2 && snapshot.concurrent === 0 && snapshot.spent === 2 && snapshot.reserved === 0, 'completed work reconciles usage and releases slots');
491
+ const omitted = fixture(c);
492
+ omitted.read({ maxCost: 3 });
493
+ const omittedTask = omitted.task(readGrant(), { budget: omitted.budget({ maxCost: 6 }) });
494
+ await omitted.invoke(omittedTask, 'read_document', { resource: GUIDE });
495
+ await omitted.invoke(omittedTask, 'read_document', { resource: GUIDE });
496
+ const omittedSnapshot = omitted.guard.snapshot(omittedTask);
497
+ c.assert(omittedSnapshot.calls === 2 && omittedSnapshot.spent === 6 && omittedSnapshot.reserved === 0 && omittedSnapshot.concurrent === 0, 'omitted actual cost charges the full declared maximum');
488
498
  const cancel = fixture(c);
489
499
  const entered = deferred();
490
500
  let observed = false;
@@ -532,11 +542,11 @@ const cases = {
532
542
  c.assert(deadlineObserved && deadline.guard.snapshot(deadlineTask).concurrent === 0, 'deadline signals cooperative cancellation and releases slot');
533
543
  const failed = fixture(c);
534
544
  let first = true;
535
- failed.read({ execute: () => { c.handlerInvocations += 1; if (first) {
545
+ failed.read({ maxCost: 3, execute: () => { c.handlerInvocations += 1; if (first) {
536
546
  first = false;
537
547
  throw new Error('synthetic failure');
538
548
  } return { value: 'recovered' }; } });
539
- const failedTask = failed.task(readGrant(), { budget: failed.budget({ maxConcurrent: 1 }) });
549
+ const failedTask = failed.task(readGrant(), { budget: failed.budget({ maxConcurrent: 1, maxCost: 9 }) });
540
550
  let rejected = false;
541
551
  try {
542
552
  await failed.invoke(failedTask, 'read_document', { resource: GUIDE });
@@ -545,16 +555,151 @@ const cases = {
545
555
  rejected = true;
546
556
  }
547
557
  c.assert(rejected && failed.guard.snapshot(failedTask).calls === 1 && failed.guard.snapshot(failedTask).concurrent === 0, 'failed handler counts attempt and releases concurrency');
558
+ c.assert(failed.guard.snapshot(failedTask).spent === 3 && failed.guard.snapshot(failedTask).reserved === 0, 'failed handler retains the full declared charge');
548
559
  c.assert(await failed.invoke(failedTask, 'read_document', { resource: GUIDE }) === 'recovered', 'next authorized call can use released slot');
560
+ c.assert(failed.guard.snapshot(failedTask).spent === 6 && failed.guard.snapshot(failedTask).concurrent === 0, 'recovery retains both admitted charges and releases concurrency');
561
+ },
562
+ },
563
+ S11: {
564
+ async malicious(c) {
565
+ const f = fixture(c);
566
+ let resolutions = 0;
567
+ f.register({ name: 'typed_write', description: 'Write a typed synthetic record',
568
+ parse(input) {
569
+ const value = fields(input);
570
+ if (Object.keys(value).some(key => !['resource', 'quantity'].includes(key)) ||
571
+ typeof value.resource !== 'string' || !value.resource.trim() ||
572
+ typeof value.quantity !== 'number' || !Number.isSafeInteger(value.quantity) || value.quantity < 1 || value.quantity > 10) {
573
+ throw new Error(SECRET);
574
+ }
575
+ return { resource: value.resource.trim(), quantity: value.quantity };
576
+ },
577
+ resolve(args) { resolutions++; return { resourceId: stringField(args, 'resource'), tenantId: 'alpha' }; },
578
+ execute: args => c.effect(args),
579
+ });
580
+ const task = f.task([{ tool: 'typed_write', resources: [RECORD_A] }]);
581
+ for (const args of [
582
+ { resource: RECORD_A, quantity: '1' }, { resource: RECORD_A, quantity: 1, command: 'unexpected' },
583
+ { quantity: 1 }, { resource: RECORD_A, quantity: 0 }, { resource: RECORD_A, quantity: 1.5 },
584
+ ])
585
+ await c.denied(() => f.invoke(task, 'typed_write', args), 'invalid typed arguments', 'ARGUMENT_INVALID');
586
+ c.assert(resolutions === 0, 'invalid typed arguments never reach resource resolution');
587
+ for (const parse of [() => ({ unsafe: undefined }), () => Promise.resolve(null)]) {
588
+ f.register({ name: 'invalid_parser', description: 'Invalid trusted parser fixture', parse,
589
+ resolve: () => { resolutions++; return { resourceId: RECORD_A, tenantId: 'alpha' }; }, execute: args => c.effect(args) });
590
+ const parserTask = f.task([{ tool: 'invalid_parser', resources: [RECORD_A] }]);
591
+ await c.denied(() => f.invoke(parserTask, 'invalid_parser', null), 'invalid parser result', 'ARGUMENT_INVALID');
592
+ }
593
+ c.assert(resolutions === 0 && c.handlerInvocations === 0, 'parser failures prevent resolution and effects');
594
+ },
595
+ async benign(c) {
596
+ const observed = [];
597
+ const f = fixture(c, { policy: action => { observed.push(action.args); return 'allow'; } });
598
+ let parses = 0;
599
+ let normalized;
600
+ f.register({ name: 'typed_write', description: 'Write a normalized synthetic record', requiresApproval: true,
601
+ parse(input) {
602
+ parses++;
603
+ const value = fields(input);
604
+ if (typeof value.resource !== 'string' || !Number.isSafeInteger(value.quantity))
605
+ throw new Error('invalid fixture');
606
+ normalized = { resource: value.resource.trim(), quantity: Number(value.quantity) };
607
+ return normalized;
608
+ },
609
+ resolve(args) { return { resourceId: stringField(args, 'resource'), tenantId: 'alpha' }; },
610
+ execute(args, { action }) {
611
+ c.assert(args === action.args && Object.isFrozen(args), 'handler receives the immutable approved arguments');
612
+ f.ledger.push(`${action.resourceId}:${fields(args).quantity}`);
613
+ return c.effect(args);
614
+ },
615
+ });
616
+ const task = f.task([{ tool: 'typed_write', resources: [RECORD_A] }]);
617
+ const input = { resource: ` ${RECORD_A} `, quantity: 3 };
618
+ const prepared = await f.guard.prepare(task, { tool: 'typed_write', args: input });
619
+ c.assert(prepared.action.resourceId === RECORD_A && fields(prepared.action.args).resource === RECORD_A, 'normalized resource is resolved and bound');
620
+ const approvalId = f.approve(prepared);
621
+ input.resource = RECORD_B;
622
+ input.quantity = 9;
623
+ normalized.resource = RECORD_B;
624
+ normalized.quantity = 8;
625
+ const value = await f.guard.execute(prepared, { approvalId });
626
+ c.assert(fields(value).resource === RECORD_A && fields(value).quantity === 3, 'caller and parser mutations do not alter the approved write');
627
+ c.assert(observed.length === 2 && observed.every(args => fields(args).resource === RECORD_A && fields(args).quantity === 3), 'preparation and execution policy see normalized arguments');
628
+ c.assert(parses === 1 && c.handlerInvocations === 1 && f.ledger[0] === `${RECORD_A}:3`, 'one parser pass and one normalized approved effect');
629
+ },
630
+ },
631
+ S12: {
632
+ async malicious(c) {
633
+ const f = fixture(c);
634
+ let version = 'v1';
635
+ f.register({ name: 'versioned_write', description: 'Compare and write one synthetic record', requiresApproval: true,
636
+ resolve: () => ({ resourceId: RECORD_A, tenantId: 'alpha', resourceVersion: version }),
637
+ execute(_args, { action }) {
638
+ c.handlerInvocations++;
639
+ if (action.resourceVersion !== version)
640
+ throw new Error('synthetic version conflict');
641
+ f.ledger.push(`write:${version}`);
642
+ return { value: version };
643
+ },
644
+ });
645
+ const task = f.task([{ tool: 'versioned_write', resources: [RECORD_A] }]);
646
+ const old = await f.guard.prepare(task, { tool: 'versioned_write', args: null });
647
+ const approvalId = f.approve(old);
648
+ c.assert(old.action.resourceVersion === 'v1' && Object.isFrozen(old.action), 'resolved version is immutable approved action data');
649
+ await c.denied(() => f.guard.execute({ ...old, action: { ...old.action, resourceVersion: 'v2' } }, { approvalId }), 'forged resource version', 'INVALID_PREPARED');
650
+ version = 'v2';
651
+ const fresh = await f.guard.prepare(task, { tool: 'versioned_write', args: null });
652
+ c.assert(fresh.action.resourceVersion === 'v2' && fresh.digest !== old.digest, 'new resource version receives a distinct prepared invocation');
653
+ await c.denied(() => f.guard.execute(fresh, { approvalId }), 'approval transfer to new version', 'APPROVAL_INVALID');
654
+ let error;
655
+ try {
656
+ await f.guard.execute(old, { approvalId });
657
+ }
658
+ catch (caught) {
659
+ error = caught;
660
+ }
661
+ c.assert(error instanceof GuardError && error.code === 'TOOL_FAILED', 'stale resource compare fails with a sanitized handler error');
662
+ c.denialCodes.push('TOOL_FAILED');
663
+ c.assert(c.handlerInvocations === 1 && f.ledger.length === 0, 'stale attempt enters the handler but produces no effect');
664
+ await c.denied(() => f.guard.execute(old, { approvalId }), 'stale attempt cannot replay', 'ACTION_USED');
665
+ c.assert(f.guard.snapshot(task).calls === 1, 'stale admitted attempt still consumes one call');
666
+ },
667
+ async benign(c) {
668
+ const f = fixture(c);
669
+ let version = 'v1';
670
+ f.register({ name: 'versioned_write', description: 'Compare and write one synthetic record', requiresApproval: true,
671
+ resolve: () => ({ resourceId: RECORD_A, tenantId: 'alpha', resourceVersion: version }),
672
+ execute(_args, { action }) {
673
+ c.handlerInvocations++;
674
+ if (action.resourceVersion !== version)
675
+ throw new Error('synthetic version conflict');
676
+ f.ledger.push(`write:${version}`);
677
+ version = version === 'v1' ? 'v2' : 'v3';
678
+ return { value: { version } };
679
+ },
680
+ });
681
+ const task = f.task([{ tool: 'versioned_write', resources: [RECORD_A] }]);
682
+ for (const expected of ['v1', 'v2']) {
683
+ const prepared = await f.guard.prepare(task, { tool: 'versioned_write', args: null });
684
+ c.assert(prepared.action.resourceVersion === expected, 'preparation binds the current resource version');
685
+ const approvalId = f.approve(prepared);
686
+ await f.guard.execute(prepared, { approvalId });
687
+ }
688
+ c.assert(c.handlerInvocations === 2 && f.ledger.join(',') === 'write:v1,write:v2', 'each reviewed version produces exactly one intended effect');
689
+ c.assert(version === 'v3' && f.guard.snapshot(task).calls === 2, 'two independent approvals advance the synthetic resource twice');
549
690
  },
550
691
  },
551
692
  };
552
693
  /**
553
- * Run synthetic effects against this installation's public core API.
554
- * This validates the listed local fixtures, not a deployment, transport, or universal injection defense.
555
- * No network, credentials, filesystem writes, model calls, or external effects are used.
694
+ * Run the catalog fixtures against AgentGuard or the supplied adapter.
695
+ * The default implementation uses in-memory effects only. A custom adapter is
696
+ * responsible for its own I/O; the runner cannot isolate or cancel it.
556
697
  */
557
698
  export async function runConformance(options = {}) {
699
+ const caseTimeoutMs = options.caseTimeoutMs === undefined ? 5000 : options.caseTimeoutMs;
700
+ if (!Number.isSafeInteger(caseTimeoutMs) || caseTimeoutMs < 1 || caseTimeoutMs > 2_147_483_647) {
701
+ throw new TypeError('caseTimeoutMs must be an integer from 1 through 2147483647');
702
+ }
558
703
  const selected = options.scenarioIds ?? scenarioCatalog.map((scenario) => scenario.id);
559
704
  if (selected.length === 0 || new Set(selected).size !== selected.length || selected.some((id) => !Object.hasOwn(cases, id))) {
560
705
  throw new TypeError('scenarioIds must be a nonempty list of distinct catalog identifiers');
@@ -565,7 +710,7 @@ export async function runConformance(options = {}) {
565
710
  const checks = new Checks(options.createGuard);
566
711
  let failure;
567
712
  try {
568
- await cases[scenarioId][variant](checks);
713
+ await bounded(cases[scenarioId][variant](checks), 'case exceeded its asynchronous timeout', caseTimeoutMs);
569
714
  }
570
715
  catch (error) {
571
716
  failure = error instanceof CheckFailure ? error.message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shieldaai/agent-guard-scenarios",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Executable protective scenarios and conformance fixtures for agent tool executors.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -43,9 +43,10 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",
46
+ "prepack": "node ../../scripts/build.mjs",
46
47
  "prepublishOnly": "node ../../scripts/release-check.mjs"
47
48
  },
48
49
  "dependencies": {
49
- "@shieldaai/agent-guard": "0.1.0-alpha.0"
50
+ "@shieldaai/agent-guard": "0.1.0-alpha.2"
50
51
  }
51
52
  }