@shieldaai/agent-guard-scenarios 0.1.0-alpha.0 → 0.1.0-alpha.1
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 +5 -1
- package/dist/catalog.d.ts +2 -2
- package/dist/catalog.js +2 -2
- package/dist/index.d.ts +7 -5
- package/dist/index.js +38 -22
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,12 +11,16 @@ 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.
|
|
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. 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
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.
|
|
17
17
|
|
|
18
|
+
The catalog version identifies the executed acceptance checks independently of the package version; compare reports using their `catalogVersion`. Version `1.1.0` strengthens the existing S08 race assertions and S10 charging checks and adds the per-variant timeout. It retains ten scenario identifiers and twenty paired cases. A pass from an older catalog does not establish coverage of newly added assertions.
|
|
19
|
+
|
|
18
20
|
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
21
|
|
|
22
|
+
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.
|
|
23
|
+
|
|
20
24
|
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
25
|
|
|
22
26
|
Apache-2.0. Copyright 2026 ShieldaAI contributors.
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Version of the synthetic acceptance cases, independent of package releases. */
|
|
2
|
-
export declare const SCENARIO_CATALOG_VERSION: '1.
|
|
2
|
+
export declare const SCENARIO_CATALOG_VERSION: '1.1.0';
|
|
3
3
|
export type ScenarioId = 'S01' | 'S02' | 'S03' | 'S04' | 'S05' | 'S06' | 'S07' | 'S08' | 'S09' | 'S10';
|
|
4
4
|
export interface ScenarioDefinition {
|
|
5
5
|
readonly id: ScenarioId;
|
|
@@ -9,5 +9,5 @@ export interface ScenarioDefinition {
|
|
|
9
9
|
readonly benign: string;
|
|
10
10
|
readonly limitations: readonly string[];
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
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.
|
|
2
|
+
export const SCENARIO_CATALOG_VERSION = '1.1.0';
|
|
3
3
|
const definitions = [
|
|
4
4
|
{
|
|
5
5
|
id: 'S01', title: 'Deny unknown tools and unauthorized resources',
|
|
@@ -72,7 +72,7 @@ const definitions = [
|
|
|
72
72
|
limitations: ['Quotas are local and nondurable; the provider must enforce declared upper costs and handlers must honor cancellation.'],
|
|
73
73
|
},
|
|
74
74
|
];
|
|
75
|
-
/**
|
|
75
|
+
/** Read-only scenario descriptions. Call runConformance() to run the fixtures. */
|
|
76
76
|
export const scenarioCatalog = Object.freeze(definitions.map((entry) => Object.freeze({
|
|
77
77
|
...entry,
|
|
78
78
|
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
|
-
/**
|
|
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
9
|
/** Omit to execute all ten 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
|
-
*
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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)),
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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,22 @@ 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');
|
|
549
561
|
},
|
|
550
562
|
},
|
|
551
563
|
};
|
|
552
564
|
/**
|
|
553
|
-
* Run
|
|
554
|
-
*
|
|
555
|
-
*
|
|
565
|
+
* Run the catalog fixtures against AgentGuard or the supplied adapter.
|
|
566
|
+
* The default implementation uses in-memory effects only. A custom adapter is
|
|
567
|
+
* responsible for its own I/O; the runner cannot isolate or cancel it.
|
|
556
568
|
*/
|
|
557
569
|
export async function runConformance(options = {}) {
|
|
570
|
+
const caseTimeoutMs = options.caseTimeoutMs === undefined ? 5000 : options.caseTimeoutMs;
|
|
571
|
+
if (!Number.isSafeInteger(caseTimeoutMs) || caseTimeoutMs < 1 || caseTimeoutMs > 2_147_483_647) {
|
|
572
|
+
throw new TypeError('caseTimeoutMs must be an integer from 1 through 2147483647');
|
|
573
|
+
}
|
|
558
574
|
const selected = options.scenarioIds ?? scenarioCatalog.map((scenario) => scenario.id);
|
|
559
575
|
if (selected.length === 0 || new Set(selected).size !== selected.length || selected.some((id) => !Object.hasOwn(cases, id))) {
|
|
560
576
|
throw new TypeError('scenarioIds must be a nonempty list of distinct catalog identifiers');
|
|
@@ -565,7 +581,7 @@ export async function runConformance(options = {}) {
|
|
|
565
581
|
const checks = new Checks(options.createGuard);
|
|
566
582
|
let failure;
|
|
567
583
|
try {
|
|
568
|
-
await cases[scenarioId][variant](checks);
|
|
584
|
+
await bounded(cases[scenarioId][variant](checks), 'case exceeded its asynchronous timeout', caseTimeoutMs);
|
|
569
585
|
}
|
|
570
586
|
catch (error) {
|
|
571
587
|
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.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
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.
|
|
50
|
+
"@shieldaai/agent-guard": "0.1.0-alpha.1"
|
|
50
51
|
}
|
|
51
52
|
}
|