agentcert 0.2.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/cli.js +8 -0
- package/dist/command-help.js +2 -0
- package/dist/index.js +1 -0
- package/dist/sandbox.js +323 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
- package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
- package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/policies.js +109 -0
- package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
- package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/risk.js +54 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +133 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sdk.js +140 -0
- package/dist/vendor/onegent-runtime/service.d.ts +27 -0
- package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/service.js +421 -0
- package/dist/vendor/onegent-runtime/store.d.ts +16 -0
- package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/store.js +29 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
- package/dist/vendor/onegent-runtime/types.d.ts +284 -0
- package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/types.js +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,43 @@ or mismatched artifacts remain observable as `partial` or `rejected`.
|
|
|
57
57
|
Project API keys can create runs, record events, and upload evidence, but
|
|
58
58
|
cannot approve their own runtime actions.
|
|
59
59
|
|
|
60
|
+
## Sandbox onboarding
|
|
61
|
+
|
|
62
|
+
Create and certify a synthetic SandboxSystem adapter with the same public CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx agentcert sandbox init
|
|
66
|
+
npx agentcert sandbox certify --adapter ./agentcert.sandbox.mjs
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The first command writes one dependency-free JavaScript file. The second runs
|
|
70
|
+
the bundled deterministic adapter contract and writes
|
|
71
|
+
`.agentcert/sandbox/sandbox-adapter-conformance.json`. No `@agentcert` scoped
|
|
72
|
+
package is required.
|
|
73
|
+
|
|
74
|
+
After `agentcert connect`, certify and upload in one command:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx agentcert sandbox push --adapter ./agentcert.sandbox.mjs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Passing and failing reports are both retained for review, while failed
|
|
81
|
+
certifications return a non-zero exit status. The generated template is
|
|
82
|
+
synthetic and network-denied; it must not contain production credentials or
|
|
83
|
+
connect to live systems.
|
|
84
|
+
|
|
85
|
+
Run one bounded read against an existing Stripe sandbox PaymentIntent:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
STRIPE_RESTRICTED_TEST_KEY="rk_test_..." npx agentcert sandbox stripe-readonly --payment-intent pi_...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Add `--push` to retain the redacted report in AgentCert Hosted. The command
|
|
92
|
+
allows only `https://api.stripe.com`, `GET`, and allowlisted PaymentIntent
|
|
93
|
+
routes, with a 5-second timeout and 10-request-per-minute process-local cap.
|
|
94
|
+
The Stripe key is environment-only and is never written to output or evidence.
|
|
95
|
+
See the [Bounded Vendor Sandbox Egress guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/bounded-vendor-sandbox-egress.md).
|
|
96
|
+
|
|
60
97
|
Review/export helpers:
|
|
61
98
|
|
|
62
99
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { parseSchemaId, validateAgentCertSchema } from "./schema-validator.js";
|
|
|
22
22
|
import { applyRunOverrides, loadRunProfile, profileFromArtifactFlags, renderRunSummary, runAgentCertProfile, } from "./runner.js";
|
|
23
23
|
import { buildRobustnessLabSnapshot, readRobustnessLabConfig, renderRobustnessLabSummary, writeRobustnessLabSnapshot } from "./lab.js";
|
|
24
24
|
import { renderCommandHelp } from "./command-help.js";
|
|
25
|
+
import { runSandboxCommand } from "./sandbox.js";
|
|
25
26
|
process.on("uncaughtException", reportFatalError);
|
|
26
27
|
process.on("unhandledRejection", reportFatalError);
|
|
27
28
|
const command = process.argv[2] ?? "help";
|
|
@@ -120,6 +121,10 @@ Saved connections are reused by agentcert push and agentcert run --push.
|
|
|
120
121
|
process.stdout.write(`Credentials: ${path}\n`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
else if (command === "sandbox") {
|
|
125
|
+
const result = await runSandboxCommand(process.argv.slice(3));
|
|
126
|
+
process.exitCode = result.exitCode;
|
|
127
|
+
}
|
|
123
128
|
else if (command === "report") {
|
|
124
129
|
const config = await loadConfig(readFlag("--config"));
|
|
125
130
|
const subject = readFlag("--subject") ?? config?.subject.name ?? "agentcert-subject";
|
|
@@ -546,6 +551,9 @@ else {
|
|
|
546
551
|
process.stdout.write(`Usage:
|
|
547
552
|
agentcert init --subject my-browser-agent
|
|
548
553
|
agentcert connect --server https://agentcert-control-plane.onrender.com --project <project-id>
|
|
554
|
+
agentcert sandbox init
|
|
555
|
+
agentcert sandbox certify --adapter ./agentcert.sandbox.mjs
|
|
556
|
+
agentcert sandbox push --adapter ./agentcert.sandbox.mjs
|
|
549
557
|
agentcert init --out agentcert.config.json --tripwire-config tripwire.yml --force
|
|
550
558
|
agentcert init --subject my-browser-agent --github-action
|
|
551
559
|
agentcert report --mcpbench .mcpbench/latest/results.json --tripwire .tripwire/latest/tripwire-result.json --onegent .onegent/procurement/audit-packet.json --out .agentcert/latest --subject my-agent
|
package/dist/command-help.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./bundle.js";
|
|
|
2
2
|
export * from "./corpus.js";
|
|
3
3
|
export * from "./corpus-store.js";
|
|
4
4
|
export * from "./control-plane.js";
|
|
5
|
+
export * from "./sandbox.js";
|
|
5
6
|
export * from "./conformance.js";
|
|
6
7
|
export * from "./credentials.js";
|
|
7
8
|
export * from "./failure-review.js";
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { resolveConnection } from "./credentials.js";
|
|
5
|
+
const DEFAULT_ADAPTER_PATH = "agentcert.sandbox.mjs";
|
|
6
|
+
const DEFAULT_REPORT_PATH = ".agentcert/sandbox/sandbox-adapter-conformance.json";
|
|
7
|
+
const DEFAULT_STRIPE_REPORT_PATH = ".agentcert/sandbox/stripe-readonly-report.json";
|
|
8
|
+
export async function runSandboxCommand(args) {
|
|
9
|
+
const action = args[0] ?? "help";
|
|
10
|
+
if (action === "help" || args.includes("--help") || args.includes("-h")) {
|
|
11
|
+
process.stdout.write(renderSandboxHelp(action === "help" ? undefined : action));
|
|
12
|
+
return { exitCode: 0 };
|
|
13
|
+
}
|
|
14
|
+
if (action === "init")
|
|
15
|
+
return initializeSandboxAdapter(args.slice(1));
|
|
16
|
+
if (action === "certify")
|
|
17
|
+
return certifySandboxAdapter(args.slice(1));
|
|
18
|
+
if (action === "push")
|
|
19
|
+
return pushSandboxCertification(args.slice(1));
|
|
20
|
+
if (action === "stripe-readonly")
|
|
21
|
+
return runStripeSandboxReadOnly(args.slice(1));
|
|
22
|
+
throw new Error(`Unknown sandbox command ${JSON.stringify(action)}. Run \`npx agentcert sandbox --help\`.`);
|
|
23
|
+
}
|
|
24
|
+
export async function initializeSandboxAdapter(args) {
|
|
25
|
+
const requestedPath = flag(args, "--adapter") ?? flag(args, "--out") ?? DEFAULT_ADAPTER_PATH;
|
|
26
|
+
const outPath = resolve(requestedPath);
|
|
27
|
+
await writeStarterFile(outPath, sandboxAdapterTemplate(), args.includes("--force"));
|
|
28
|
+
process.stdout.write(`Wrote ${outPath}\n\nNext:\n`);
|
|
29
|
+
process.stdout.write(` npx agentcert sandbox certify --adapter ${JSON.stringify(requestedPath)}\n`);
|
|
30
|
+
process.stdout.write("\nReplace the in-memory handlers with your sandbox implementation. Keep production credentials and live systems out of this adapter.\n");
|
|
31
|
+
return { exitCode: 0 };
|
|
32
|
+
}
|
|
33
|
+
export async function certifySandboxAdapter(args, runtimeOverride) {
|
|
34
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
35
|
+
const adapterPath = resolve(flag(args, "--adapter") ?? DEFAULT_ADAPTER_PATH);
|
|
36
|
+
const reportPath = resolve(flag(args, "--out") ?? DEFAULT_REPORT_PATH);
|
|
37
|
+
const system = await loadSandboxAdapter(adapterPath);
|
|
38
|
+
const report = await runtime.runSandboxAdapterConformanceSuite({
|
|
39
|
+
system,
|
|
40
|
+
implementation: flag(args, "--implementation"),
|
|
41
|
+
targetSystem: flag(args, "--target-system"),
|
|
42
|
+
});
|
|
43
|
+
await runtime.writeSandboxAdapterConformanceReport(report, reportPath);
|
|
44
|
+
renderCertificationResult(report, reportPath);
|
|
45
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
46
|
+
}
|
|
47
|
+
export async function pushSandboxCertification(args, runtimeOverride) {
|
|
48
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
49
|
+
const certified = await certifySandboxAdapter(args, runtime);
|
|
50
|
+
if (!certified.report || !certified.reportPath)
|
|
51
|
+
throw new Error("Sandbox certification did not produce a report.");
|
|
52
|
+
const connection = await resolveConnection({
|
|
53
|
+
name: flag(args, "--connection"),
|
|
54
|
+
server: flag(args, "--server"),
|
|
55
|
+
projectId: flag(args, "--project"),
|
|
56
|
+
apiKey: flag(args, "--api-key"),
|
|
57
|
+
});
|
|
58
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(certified.report, {
|
|
59
|
+
baseUrl: connection.server,
|
|
60
|
+
projectId: connection.projectId,
|
|
61
|
+
apiKey: connection.apiKey,
|
|
62
|
+
externalId: flag(args, "--external-id"),
|
|
63
|
+
});
|
|
64
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
65
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
66
|
+
return certified;
|
|
67
|
+
}
|
|
68
|
+
export async function runStripeSandboxReadOnly(args, runtimeOverride) {
|
|
69
|
+
if (args.includes("--stripe-key") || args.includes("--restricted-key")) {
|
|
70
|
+
throw new Error("Stripe credentials are accepted only through STRIPE_RESTRICTED_TEST_KEY, never a CLI flag.");
|
|
71
|
+
}
|
|
72
|
+
const paymentIntentId = flag(args, "--payment-intent");
|
|
73
|
+
if (!paymentIntentId)
|
|
74
|
+
throw new Error("Stripe sandbox read requires --payment-intent <pi_...>.");
|
|
75
|
+
const restrictedApiKey = process.env.STRIPE_RESTRICTED_TEST_KEY?.trim();
|
|
76
|
+
if (!restrictedApiKey) {
|
|
77
|
+
throw new Error("Set STRIPE_RESTRICTED_TEST_KEY to a read-only Stripe rk_test_ restricted key.");
|
|
78
|
+
}
|
|
79
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
80
|
+
if (!runtime.runStripeSandboxReadOnlyCertification) {
|
|
81
|
+
throw new Error("AgentCert Stripe sandbox runtime is missing. Reinstall the agentcert package and retry.");
|
|
82
|
+
}
|
|
83
|
+
const report = await runtime.runStripeSandboxReadOnlyCertification({ restrictedApiKey, paymentIntentId });
|
|
84
|
+
const reportPath = resolve(flag(args, "--out") ?? DEFAULT_STRIPE_REPORT_PATH);
|
|
85
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
86
|
+
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
87
|
+
renderVendorReadResult(report, reportPath);
|
|
88
|
+
if (args.includes("--push")) {
|
|
89
|
+
const connection = await resolveConnection({
|
|
90
|
+
name: flag(args, "--connection"),
|
|
91
|
+
server: flag(args, "--server"),
|
|
92
|
+
projectId: flag(args, "--project"),
|
|
93
|
+
apiKey: flag(args, "--api-key"),
|
|
94
|
+
});
|
|
95
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(report, {
|
|
96
|
+
baseUrl: connection.server,
|
|
97
|
+
projectId: connection.projectId,
|
|
98
|
+
apiKey: connection.apiKey,
|
|
99
|
+
externalId: flag(args, "--external-id"),
|
|
100
|
+
});
|
|
101
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
102
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
103
|
+
}
|
|
104
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
105
|
+
}
|
|
106
|
+
export async function loadSandboxAdapter(adapterPath) {
|
|
107
|
+
let module;
|
|
108
|
+
try {
|
|
109
|
+
module = await import(pathToFileURL(adapterPath).href);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
throw new Error(`Could not load sandbox adapter ${adapterPath}: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
const candidate = module.sandboxSystem ?? module.default ?? module.system;
|
|
116
|
+
if (!isSandboxSystem(candidate)) {
|
|
117
|
+
throw new Error(`Sandbox adapter ${adapterPath} must export \`sandboxSystem\`, \`system\`, or a default SandboxSystem object.`);
|
|
118
|
+
}
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
export function renderSandboxHelp(action) {
|
|
122
|
+
if (action === "init")
|
|
123
|
+
return `Usage:
|
|
124
|
+
agentcert sandbox init [--adapter agentcert.sandbox.mjs] [--force]
|
|
125
|
+
|
|
126
|
+
Writes one dependency-free synthetic SandboxSystem adapter template.
|
|
127
|
+
`;
|
|
128
|
+
if (action === "certify")
|
|
129
|
+
return `Usage:
|
|
130
|
+
agentcert sandbox certify --adapter ./my-sandbox-adapter.js [--out .agentcert/sandbox/report.json]
|
|
131
|
+
|
|
132
|
+
Runs the deterministic sandbox adapter conformance suite locally.
|
|
133
|
+
`;
|
|
134
|
+
if (action === "push")
|
|
135
|
+
return `Usage:
|
|
136
|
+
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
137
|
+
|
|
138
|
+
Runs certification, writes the local report, and uploads it through the saved AgentCert connection.
|
|
139
|
+
Use \`agentcert connect\` first, or pass --server, --project, and --api-key.
|
|
140
|
+
`;
|
|
141
|
+
if (action === "stripe-readonly")
|
|
142
|
+
return `Usage:
|
|
143
|
+
STRIPE_RESTRICTED_TEST_KEY=rk_test_... agentcert sandbox stripe-readonly --payment-intent pi_...
|
|
144
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... --push
|
|
145
|
+
|
|
146
|
+
Reads one Stripe sandbox PaymentIntent through a fixed GET/resource allowlist,
|
|
147
|
+
writes a redacted evidence report, and optionally uploads it to AgentCert Hosted.
|
|
148
|
+
The Stripe key is read only from STRIPE_RESTRICTED_TEST_KEY.
|
|
149
|
+
`;
|
|
150
|
+
return `Usage:
|
|
151
|
+
agentcert sandbox init [--adapter agentcert.sandbox.mjs]
|
|
152
|
+
agentcert sandbox certify --adapter ./my-sandbox-adapter.js
|
|
153
|
+
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
154
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... [--push]
|
|
155
|
+
|
|
156
|
+
Commands:
|
|
157
|
+
init Write one dependency-free adapter template
|
|
158
|
+
certify Run deterministic local conformance checks
|
|
159
|
+
push Certify and upload the report to AgentCert Hosted
|
|
160
|
+
stripe-readonly Run one bounded Stripe sandbox read and write evidence
|
|
161
|
+
|
|
162
|
+
Common options:
|
|
163
|
+
--adapter <path> Adapter module (default: agentcert.sandbox.mjs)
|
|
164
|
+
--out <path> Local report path
|
|
165
|
+
--implementation <id> Stable adapter implementation name
|
|
166
|
+
--target-system <name> Target system exercised by the suite
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
async function loadSandboxRuntime() {
|
|
170
|
+
const adapterKitUrl = new URL("./vendor/onegent-runtime/sandbox-adapter-kit.js", import.meta.url);
|
|
171
|
+
const hostedUrl = new URL("./vendor/onegent-runtime/sandbox-hosted.js", import.meta.url);
|
|
172
|
+
const stripeUrl = new URL("./vendor/onegent-runtime/stripe-test-readonly.js", import.meta.url);
|
|
173
|
+
try {
|
|
174
|
+
const [adapterKit, hosted, stripe] = await Promise.all([
|
|
175
|
+
import(adapterKitUrl.href),
|
|
176
|
+
import(hostedUrl.href),
|
|
177
|
+
import(stripeUrl.href),
|
|
178
|
+
]);
|
|
179
|
+
return { ...adapterKit, ...hosted, ...stripe };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
183
|
+
if (code === "ERR_MODULE_NOT_FOUND") {
|
|
184
|
+
throw new Error("AgentCert sandbox runtime is missing. Reinstall the agentcert package and retry.");
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isSandboxSystem(value) {
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
191
|
+
return false;
|
|
192
|
+
const system = value;
|
|
193
|
+
const safety = system.safety;
|
|
194
|
+
return typeof system.name === "string"
|
|
195
|
+
&& safety?.mode === "sandbox"
|
|
196
|
+
&& safety.networkAccess === false
|
|
197
|
+
&& safety.syntheticDataOnly === true
|
|
198
|
+
&& Array.isArray(safety.allowedTargetSystems)
|
|
199
|
+
&& ["createTenant", "deleteTenant", "resetTenant", "seedTenant", "hasTenant", "snapshotTenant", "adapterForTenant"]
|
|
200
|
+
.every((method) => typeof system[method] === "function");
|
|
201
|
+
}
|
|
202
|
+
function flag(args, name) {
|
|
203
|
+
const index = args.indexOf(name);
|
|
204
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
205
|
+
}
|
|
206
|
+
async function writeStarterFile(path, content, force) {
|
|
207
|
+
await mkdir(dirname(path), { recursive: true });
|
|
208
|
+
try {
|
|
209
|
+
await writeFile(path, content, { flag: force ? "w" : "wx" });
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
213
|
+
throw new Error(`${path} already exists. Re-run with --force to overwrite it.`);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function renderCertificationResult(report, reportPath) {
|
|
219
|
+
process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 sandbox adapter conformance\n`);
|
|
220
|
+
for (const check of report.checks) {
|
|
221
|
+
process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(`Report: ${reportPath}\n`);
|
|
224
|
+
}
|
|
225
|
+
function renderVendorReadResult(report, reportPath) {
|
|
226
|
+
process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 Stripe sandbox bounded read\n`);
|
|
227
|
+
for (const check of report.checks) {
|
|
228
|
+
process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(`Requests audited: ${report.audit.length}\n`);
|
|
231
|
+
process.stdout.write(`Report: ${reportPath}\n`);
|
|
232
|
+
}
|
|
233
|
+
function sandboxAdapterTemplate() {
|
|
234
|
+
return `// Synthetic local state only. Do not import production credentials or call live systems here.
|
|
235
|
+
const tenants = new Map();
|
|
236
|
+
|
|
237
|
+
export const sandboxSystem = {
|
|
238
|
+
name: "my-sandbox-adapter",
|
|
239
|
+
safety: {
|
|
240
|
+
mode: "sandbox",
|
|
241
|
+
networkAccess: false,
|
|
242
|
+
syntheticDataOnly: true,
|
|
243
|
+
allowedTargetSystems: ["MySandboxSystem"],
|
|
244
|
+
},
|
|
245
|
+
createTenant(input) {
|
|
246
|
+
if (input.synthetic !== true) throw new Error("Synthetic tenants only.");
|
|
247
|
+
if (tenants.has(input.id)) throw new Error(\`Tenant \${input.id} already exists.\`);
|
|
248
|
+
const seed = syntheticSeed(input.seed ?? {});
|
|
249
|
+
tenants.set(input.id, { seed, state: structuredClone(seed) });
|
|
250
|
+
},
|
|
251
|
+
deleteTenant(tenantId) {
|
|
252
|
+
tenants.delete(tenantId);
|
|
253
|
+
},
|
|
254
|
+
resetTenant(tenantId) {
|
|
255
|
+
const tenant = requiredTenant(tenantId);
|
|
256
|
+
tenant.state = structuredClone(tenant.seed);
|
|
257
|
+
},
|
|
258
|
+
seedTenant(tenantId, seed) {
|
|
259
|
+
const tenant = requiredTenant(tenantId);
|
|
260
|
+
tenant.seed = syntheticSeed(seed);
|
|
261
|
+
tenant.state = structuredClone(seed);
|
|
262
|
+
},
|
|
263
|
+
hasTenant(tenantId) {
|
|
264
|
+
return tenants.has(tenantId);
|
|
265
|
+
},
|
|
266
|
+
snapshotTenant(tenantId) {
|
|
267
|
+
return structuredClone(requiredTenant(tenantId).state);
|
|
268
|
+
},
|
|
269
|
+
adapterForTenant(tenantId) {
|
|
270
|
+
requiredTenant(tenantId);
|
|
271
|
+
return {
|
|
272
|
+
name: \`my-sandbox-adapter:\${tenantId}\`,
|
|
273
|
+
safety: { mode: "sandbox", networkAccess: false, allowedTargetSystems: ["MySandboxSystem"] },
|
|
274
|
+
execute(action) {
|
|
275
|
+
const tenant = requiredTenant(tenantId);
|
|
276
|
+
const previousState = structuredClone(tenant.state[action.businessObjectId] ?? action.beforeState);
|
|
277
|
+
const observedState = structuredClone(action.proposedAfterState);
|
|
278
|
+
tenant.state[action.businessObjectId] = observedState;
|
|
279
|
+
return { method: "SYNTHETIC_SANDBOX", targetSystem: action.targetSystem, previousState, observedState };
|
|
280
|
+
},
|
|
281
|
+
rollback(action, execution) {
|
|
282
|
+
const restored = structuredClone(execution.previousState ?? action.beforeState);
|
|
283
|
+
requiredTenant(tenantId).state[action.businessObjectId] = restored;
|
|
284
|
+
return { success: true, observedState: restored };
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default sandboxSystem;
|
|
291
|
+
|
|
292
|
+
function requiredTenant(tenantId) {
|
|
293
|
+
const tenant = tenants.get(tenantId);
|
|
294
|
+
if (!tenant) throw new Error(\`Tenant \${tenantId} does not exist.\`);
|
|
295
|
+
return tenant;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function syntheticSeed(value) {
|
|
299
|
+
assertSynthetic(value, "seed", new Set());
|
|
300
|
+
return structuredClone(value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function assertSynthetic(value, path, seen) {
|
|
304
|
+
if (value === null || ["boolean", "number"].includes(typeof value)) return;
|
|
305
|
+
if (typeof value === "string") {
|
|
306
|
+
if (/^(?:sk-(?:proj-)?|npm_|gh[pousr]_)[A-Za-z0-9_-]{16,}$/.test(value)) {
|
|
307
|
+
throw new Error(\`Credential-like value is not allowed at \${path}.\`);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (typeof value !== "object") throw new Error(\`Synthetic data must be JSON-compatible at \${path}.\`);
|
|
312
|
+
if (seen.has(value)) throw new Error(\`Synthetic data cannot contain cycles at \${path}.\`);
|
|
313
|
+
seen.add(value);
|
|
314
|
+
for (const [key, item] of Object.entries(value)) {
|
|
315
|
+
if (/(?:password|passwd|secret|token|api[_-]?key|credential|authorization|cookie)/i.test(key)) {
|
|
316
|
+
throw new Error(\`Credential-like field \${path}.\${key} is not allowed.\`);
|
|
317
|
+
}
|
|
318
|
+
assertSynthetic(item, \`\${path}.\${key}\`, seen);
|
|
319
|
+
}
|
|
320
|
+
seen.delete(value);
|
|
321
|
+
}
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ActionIntent, MockPurchaseOrder } from "./types.js";
|
|
2
|
+
export declare const PROCUREMENT_DEMO_PO_ID = "PO-DEMO-4850";
|
|
3
|
+
export declare function createProcurementDemoPurchaseOrder(): MockPurchaseOrder;
|
|
4
|
+
export declare function getPurchaseOrder(id: string): MockPurchaseOrder | undefined;
|
|
5
|
+
export declare function submitMockPurchaseOrder(action: ActionIntent): MockPurchaseOrder;
|
|
6
|
+
//# sourceMappingURL=mock-procurement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-procurement.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/mock-procurement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGlE,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AAErD,wBAAgB,kCAAkC,IAAI,iBAAiB,CActE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAE1E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,GAAG,iBAAiB,CAc/E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { nowIso, store } from "./store.js";
|
|
2
|
+
export const PROCUREMENT_DEMO_PO_ID = "PO-DEMO-4850";
|
|
3
|
+
export function createProcurementDemoPurchaseOrder() {
|
|
4
|
+
const purchaseOrder = {
|
|
5
|
+
id: PROCUREMENT_DEMO_PO_ID,
|
|
6
|
+
vendor: "Acme Industrial Supply",
|
|
7
|
+
amount: 4_850,
|
|
8
|
+
currency: "USD",
|
|
9
|
+
status: "DRAFT",
|
|
10
|
+
vendorApproved: true,
|
|
11
|
+
lineItem: "Replacement conveyor motor assembly",
|
|
12
|
+
lastUpdatedAt: nowIso(),
|
|
13
|
+
};
|
|
14
|
+
store.purchaseOrders.set(purchaseOrder.id, purchaseOrder);
|
|
15
|
+
return purchaseOrder;
|
|
16
|
+
}
|
|
17
|
+
export function getPurchaseOrder(id) {
|
|
18
|
+
return store.purchaseOrders.get(id);
|
|
19
|
+
}
|
|
20
|
+
export function submitMockPurchaseOrder(action) {
|
|
21
|
+
const purchaseOrder = store.purchaseOrders.get(action.businessObjectId);
|
|
22
|
+
if (!purchaseOrder) {
|
|
23
|
+
throw new Error(`Mock purchase order ${action.businessObjectId} was not found.`);
|
|
24
|
+
}
|
|
25
|
+
const updated = {
|
|
26
|
+
...purchaseOrder,
|
|
27
|
+
status: "SUBMITTED",
|
|
28
|
+
lastUpdatedAt: nowIso(),
|
|
29
|
+
actionIntentId: action.id,
|
|
30
|
+
};
|
|
31
|
+
store.purchaseOrders.set(updated.id, updated);
|
|
32
|
+
return updated;
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ActionIntent, PolicyEvaluation, PolicyRule, RiskAssessment } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_POLICY_RULES: PolicyRule[];
|
|
3
|
+
export declare function evaluatePolicy(action: ActionIntent, risk: RiskAssessment, policyRules?: PolicyRule[]): PolicyEvaluation;
|
|
4
|
+
export declare function getPolicyRules(ids: string[], policyRules?: PolicyRule[]): PolicyRule[];
|
|
5
|
+
export declare function matchesRule(action: ActionIntent, rule: PolicyRule): boolean;
|
|
6
|
+
//# sourceMappingURL=policies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/policies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAmB,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9G,eAAO,MAAM,oBAAoB,EAAE,UAAU,EAmC5C,CAAC;AAEF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,cAAc,EACpB,WAAW,GAAE,UAAU,EAAyB,GAC/C,gBAAgB,CA6ClB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,UAAU,EAAyB,GAAG,UAAU,EAAE,CAG5G;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAQ3E"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const DEFAULT_POLICY_RULES = [
|
|
2
|
+
{
|
|
3
|
+
id: "po-over-1000-requires-approval",
|
|
4
|
+
name: "Purchase orders over $1,000 require approval",
|
|
5
|
+
description: "High-value purchase order submissions must be reviewed before mock execution.",
|
|
6
|
+
actionTypes: ["SUBMIT"],
|
|
7
|
+
effect: "REQUIRE_APPROVAL",
|
|
8
|
+
enabled: true,
|
|
9
|
+
conditions: [{ field: "amount", operator: "greaterThan", value: 1_000 }],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "payment-actions-require-approval",
|
|
13
|
+
name: "Payment actions require approval",
|
|
14
|
+
description: "The demo runtime never executes real payments and requires approval for mock payment actions.",
|
|
15
|
+
actionTypes: ["PAY"],
|
|
16
|
+
effect: "REQUIRE_APPROVAL",
|
|
17
|
+
enabled: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "external-send-requires-approval",
|
|
21
|
+
name: "External send actions require approval",
|
|
22
|
+
description: "Outbound communications to non-demo recipients require approval.",
|
|
23
|
+
actionTypes: ["SEND"],
|
|
24
|
+
effect: "REQUIRE_APPROVAL",
|
|
25
|
+
enabled: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "production-actions-blocked-in-demo",
|
|
29
|
+
name: "Production actions are blocked in demo mode",
|
|
30
|
+
description: "This package is local-only and will not perform production actions.",
|
|
31
|
+
actionTypes: ["SUBMIT", "PAY", "SEND", "UPDATE"],
|
|
32
|
+
effect: "BLOCK",
|
|
33
|
+
enabled: true,
|
|
34
|
+
conditions: [{ field: "environment", operator: "equals", value: "production" }],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
export function evaluatePolicy(action, risk, policyRules = DEFAULT_POLICY_RULES) {
|
|
38
|
+
if (action.environment === "production") {
|
|
39
|
+
const productionBlock = policyRules.find((rule) => rule.id === "production-actions-blocked-in-demo") ??
|
|
40
|
+
DEFAULT_POLICY_RULES.find((rule) => rule.id === "production-actions-blocked-in-demo");
|
|
41
|
+
return {
|
|
42
|
+
effect: "BLOCK",
|
|
43
|
+
triggeredPolicies: productionBlock ? [productionBlock.id] : [],
|
|
44
|
+
reasons: [productionBlock?.description ?? "Production actions are blocked in this local demo runtime."],
|
|
45
|
+
requiresHumanApproval: false,
|
|
46
|
+
blocked: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const triggeredRules = policyRules.filter((rule) => matchesRule(action, rule));
|
|
50
|
+
const blockRules = triggeredRules.filter((rule) => rule.effect === "BLOCK");
|
|
51
|
+
if (blockRules.length > 0) {
|
|
52
|
+
return {
|
|
53
|
+
effect: "BLOCK",
|
|
54
|
+
triggeredPolicies: blockRules.map((rule) => rule.id),
|
|
55
|
+
reasons: blockRules.map((rule) => rule.description),
|
|
56
|
+
requiresHumanApproval: false,
|
|
57
|
+
blocked: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const approvalRules = triggeredRules.filter((rule) => rule.effect === "REQUIRE_APPROVAL");
|
|
61
|
+
if (approvalRules.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
effect: "REQUIRE_APPROVAL",
|
|
64
|
+
triggeredPolicies: approvalRules.map((rule) => rule.id),
|
|
65
|
+
reasons: approvalRules.map((rule) => rule.description),
|
|
66
|
+
requiresHumanApproval: true,
|
|
67
|
+
blocked: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
effect: "ALLOW",
|
|
72
|
+
triggeredPolicies: triggeredRules.map((rule) => rule.id),
|
|
73
|
+
reasons: risk.reasons,
|
|
74
|
+
requiresHumanApproval: false,
|
|
75
|
+
blocked: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function getPolicyRules(ids, policyRules = DEFAULT_POLICY_RULES) {
|
|
79
|
+
const idSet = new Set(ids);
|
|
80
|
+
return policyRules.filter((rule) => idSet.has(rule.id));
|
|
81
|
+
}
|
|
82
|
+
export function matchesRule(action, rule) {
|
|
83
|
+
if (!rule.enabled || !rule.actionTypes.includes(action.actionType)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (!rule.conditions || rule.conditions.length === 0) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return rule.conditions.every((condition) => matchesCondition(action, condition));
|
|
90
|
+
}
|
|
91
|
+
function matchesCondition(action, condition) {
|
|
92
|
+
const actual = action[condition.field];
|
|
93
|
+
switch (condition.operator) {
|
|
94
|
+
case "equals":
|
|
95
|
+
return actual === condition.value;
|
|
96
|
+
case "notEquals":
|
|
97
|
+
return actual !== condition.value;
|
|
98
|
+
case "greaterThan":
|
|
99
|
+
return typeof actual === "number" && typeof condition.value === "number" && actual > condition.value;
|
|
100
|
+
case "greaterThanOrEqual":
|
|
101
|
+
return typeof actual === "number" && typeof condition.value === "number" && actual >= condition.value;
|
|
102
|
+
case "lessThan":
|
|
103
|
+
return typeof actual === "number" && typeof condition.value === "number" && actual < condition.value;
|
|
104
|
+
case "lessThanOrEqual":
|
|
105
|
+
return typeof actual === "number" && typeof condition.value === "number" && actual <= condition.value;
|
|
106
|
+
case "includes":
|
|
107
|
+
return typeof actual === "string" && typeof condition.value === "string" && actual.includes(condition.value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/risk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAa,MAAM,YAAY,CAAC;AAK1E,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAsD/D"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { nextId, nowIso } from "./store.js";
|
|
2
|
+
const PURCHASE_ORDER_APPROVAL_THRESHOLD = 1_000;
|
|
3
|
+
export function assessRisk(action) {
|
|
4
|
+
const reasons = [];
|
|
5
|
+
const triggeredPolicies = [];
|
|
6
|
+
let riskLevel = "LOW";
|
|
7
|
+
let riskScore = 20;
|
|
8
|
+
if (action.actionType === "PAY") {
|
|
9
|
+
riskLevel = "HIGH";
|
|
10
|
+
riskScore = 85;
|
|
11
|
+
reasons.push("Payment actions can move money and require human approval in the demo runtime.");
|
|
12
|
+
triggeredPolicies.push("payment-actions-require-approval");
|
|
13
|
+
}
|
|
14
|
+
if (action.actionType === "SEND" && action.recipient && !action.recipient.endsWith("@example.local")) {
|
|
15
|
+
riskLevel = maxRisk(riskLevel, "HIGH");
|
|
16
|
+
riskScore = Math.max(riskScore, 75);
|
|
17
|
+
reasons.push("External send actions can disclose business data and require human approval.");
|
|
18
|
+
triggeredPolicies.push("external-send-requires-approval");
|
|
19
|
+
}
|
|
20
|
+
if (action.actionType === "SUBMIT" && (action.amount ?? 0) > PURCHASE_ORDER_APPROVAL_THRESHOLD) {
|
|
21
|
+
riskLevel = maxRisk(riskLevel, "HIGH");
|
|
22
|
+
riskScore = Math.max(riskScore, 80);
|
|
23
|
+
reasons.push("Purchase orders over $1,000 require human approval.");
|
|
24
|
+
triggeredPolicies.push("po-over-1000-requires-approval");
|
|
25
|
+
}
|
|
26
|
+
if (action.actionType === "UPDATE" && action.fieldsChanged.length > 0) {
|
|
27
|
+
riskLevel = maxRisk(riskLevel, "MEDIUM");
|
|
28
|
+
riskScore = Math.max(riskScore, 45);
|
|
29
|
+
reasons.push("Update actions modify business records and require verification.");
|
|
30
|
+
}
|
|
31
|
+
if (action.environment === "production") {
|
|
32
|
+
riskLevel = maxRisk(riskLevel, "CRITICAL");
|
|
33
|
+
riskScore = Math.max(riskScore, 95);
|
|
34
|
+
reasons.push("Production actions are blocked in this local demo runtime.");
|
|
35
|
+
triggeredPolicies.push("production-actions-blocked-in-demo");
|
|
36
|
+
}
|
|
37
|
+
if (reasons.length === 0) {
|
|
38
|
+
reasons.push("No high-risk demo policy matched this action.");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: nextId("risk"),
|
|
42
|
+
actionIntentId: action.id,
|
|
43
|
+
riskLevel,
|
|
44
|
+
riskScore,
|
|
45
|
+
reasons,
|
|
46
|
+
triggeredPolicies,
|
|
47
|
+
requiresHumanApproval: riskLevel === "HIGH" || riskLevel === "CRITICAL",
|
|
48
|
+
createdAt: nowIso(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function maxRisk(left, right) {
|
|
52
|
+
const order = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
53
|
+
return order[Math.max(order.indexOf(left), order.indexOf(right))];
|
|
54
|
+
}
|