agentcert 0.3.0 → 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 CHANGED
@@ -82,6 +82,18 @@ certifications return a non-zero exit status. The generated template is
82
82
  synthetic and network-denied; it must not contain production credentials or
83
83
  connect to live systems.
84
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
+
85
97
  Review/export helpers:
86
98
 
87
99
  ```bash
package/dist/sandbox.js CHANGED
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import { resolveConnection } from "./credentials.js";
5
5
  const DEFAULT_ADAPTER_PATH = "agentcert.sandbox.mjs";
6
6
  const DEFAULT_REPORT_PATH = ".agentcert/sandbox/sandbox-adapter-conformance.json";
7
+ const DEFAULT_STRIPE_REPORT_PATH = ".agentcert/sandbox/stripe-readonly-report.json";
7
8
  export async function runSandboxCommand(args) {
8
9
  const action = args[0] ?? "help";
9
10
  if (action === "help" || args.includes("--help") || args.includes("-h")) {
@@ -16,6 +17,8 @@ export async function runSandboxCommand(args) {
16
17
  return certifySandboxAdapter(args.slice(1));
17
18
  if (action === "push")
18
19
  return pushSandboxCertification(args.slice(1));
20
+ if (action === "stripe-readonly")
21
+ return runStripeSandboxReadOnly(args.slice(1));
19
22
  throw new Error(`Unknown sandbox command ${JSON.stringify(action)}. Run \`npx agentcert sandbox --help\`.`);
20
23
  }
21
24
  export async function initializeSandboxAdapter(args) {
@@ -62,6 +65,44 @@ export async function pushSandboxCertification(args, runtimeOverride) {
62
65
  process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
63
66
  return certified;
64
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
+ }
65
106
  export async function loadSandboxAdapter(adapterPath) {
66
107
  let module;
67
108
  try {
@@ -96,16 +137,27 @@ Runs the deterministic sandbox adapter conformance suite locally.
96
137
 
97
138
  Runs certification, writes the local report, and uploads it through the saved AgentCert connection.
98
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.
99
149
  `;
100
150
  return `Usage:
101
151
  agentcert sandbox init [--adapter agentcert.sandbox.mjs]
102
152
  agentcert sandbox certify --adapter ./my-sandbox-adapter.js
103
153
  agentcert sandbox push --adapter ./my-sandbox-adapter.js
154
+ agentcert sandbox stripe-readonly --payment-intent pi_... [--push]
104
155
 
105
156
  Commands:
106
157
  init Write one dependency-free adapter template
107
158
  certify Run deterministic local conformance checks
108
159
  push Certify and upload the report to AgentCert Hosted
160
+ stripe-readonly Run one bounded Stripe sandbox read and write evidence
109
161
 
110
162
  Common options:
111
163
  --adapter <path> Adapter module (default: agentcert.sandbox.mjs)
@@ -117,9 +169,14 @@ Common options:
117
169
  async function loadSandboxRuntime() {
118
170
  const adapterKitUrl = new URL("./vendor/onegent-runtime/sandbox-adapter-kit.js", import.meta.url);
119
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);
120
173
  try {
121
- const [adapterKit, hosted] = await Promise.all([import(adapterKitUrl.href), import(hostedUrl.href)]);
122
- return { ...adapterKit, ...hosted };
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 };
123
180
  }
124
181
  catch (error) {
125
182
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
@@ -165,6 +222,14 @@ function renderCertificationResult(report, reportPath) {
165
222
  }
166
223
  process.stdout.write(`Report: ${reportPath}\n`);
167
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
+ }
168
233
  function sandboxAdapterTemplate() {
169
234
  return `// Synthetic local state only. Do not import production credentials or call live systems here.
170
235
  const tenants = new Map();
@@ -1,7 +1,8 @@
1
1
  import type { SandboxAdapterConformanceReport } from "./sandbox-adapter-kit.js";
2
2
  import type { SandboxCertificationReport } from "./sandbox-harness.js";
3
+ import type { StripeSandboxReadOnlyReport } from "./stripe-test-readonly.js";
3
4
  export declare const SANDBOX_EVIDENCE_BUNDLE_SCHEMA_VERSION: "agentcert.evidence.v0.1";
4
- export type HostedSandboxReport = SandboxCertificationReport | SandboxAdapterConformanceReport;
5
+ export type HostedSandboxReport = SandboxCertificationReport | SandboxAdapterConformanceReport | StripeSandboxReadOnlyReport;
5
6
  export interface SandboxHostedUploadOptions {
6
7
  baseUrl: string;
7
8
  projectId: string;
@@ -48,7 +49,7 @@ export interface SandboxCertificationEvidenceBundle {
48
49
  artifacts: Record<string, string>;
49
50
  evidence: Array<{
50
51
  id: string;
51
- kind: "sandbox_adapter_conformance" | "sandbox_certification";
52
+ kind: "sandbox_adapter_conformance" | "sandbox_certification" | "sandbox_vendor_egress";
52
53
  severity: "info" | "high";
53
54
  message: string;
54
55
  source: "onegent-runtime";
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/sandbox-hosted.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sandbox-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/sandbox-hosted.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,11 @@ export function createSandboxCertificationEvidenceBundle(report) {
5
5
  const runId = `sandbox-${reportDigest}`;
6
6
  const evidence = [{
7
7
  id: `${report.kind}:${reportDigest.slice(0, 16)}`,
8
- kind: report.kind === "agentcert.sandbox_adapter_conformance" ? "sandbox_adapter_conformance" : "sandbox_certification",
8
+ kind: report.kind === "agentcert.sandbox_adapter_conformance"
9
+ ? "sandbox_adapter_conformance"
10
+ : report.kind === "agentcert.sandbox_vendor_egress"
11
+ ? "sandbox_vendor_egress"
12
+ : "sandbox_certification",
9
13
  severity: report.verdict.passed ? "info" : "high",
10
14
  message: `${report.kind} ${report.verdict.passed ? "passed" : "failed"} (${report.verdict.score}/100).`,
11
15
  source: "onegent-runtime",
@@ -0,0 +1,79 @@
1
+ import { type VendorSandboxRequestAudit } from "./vendor-sandbox-egress.js";
2
+ export declare const STRIPE_TEST_API_ORIGIN: "https://api.stripe.com";
3
+ export declare const STRIPE_SANDBOX_TIMEOUT_MS = 5000;
4
+ export declare const STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE = 10;
5
+ export declare const STRIPE_PAYMENT_INTENT_RETRIEVE: "stripe.payment_intent.retrieve";
6
+ export declare const STRIPE_PAYMENT_INTENT_LIST: "stripe.payment_intent.list";
7
+ export interface StripePaymentIntentSnapshot {
8
+ id: string;
9
+ object: "payment_intent";
10
+ livemode: false;
11
+ amount?: number;
12
+ amountCapturable?: number;
13
+ amountReceived?: number;
14
+ currency?: string;
15
+ status?: string;
16
+ created?: number;
17
+ }
18
+ export interface StripeTestModeReadOnlyAdapter {
19
+ readonly name: "stripe-test-mode-readonly";
20
+ readonly safety: {
21
+ readonly mode: "sandbox";
22
+ readonly access: "read-only";
23
+ readonly vendor: "stripe";
24
+ readonly allowedOrigins: readonly [typeof STRIPE_TEST_API_ORIGIN];
25
+ readonly allowedMethods: readonly ["GET"];
26
+ readonly allowedResources: readonly [typeof STRIPE_PAYMENT_INTENT_RETRIEVE, typeof STRIPE_PAYMENT_INTENT_LIST];
27
+ readonly timeoutMs: number;
28
+ readonly maxRequestsPerMinute: number;
29
+ };
30
+ retrievePaymentIntent(id: string): Promise<StripePaymentIntentSnapshot>;
31
+ listPaymentIntents(limit?: number): Promise<StripePaymentIntentSnapshot[]>;
32
+ getRequestAudit(): VendorSandboxRequestAudit[];
33
+ }
34
+ export interface StripeTestModeReadOnlyOptions {
35
+ restrictedApiKey: string;
36
+ fetch?: typeof fetch;
37
+ timeoutMs?: number;
38
+ maxRequestsPerMinute?: number;
39
+ now?: () => number;
40
+ }
41
+ export interface StripeSandboxReadOnlyCheck {
42
+ id: string;
43
+ status: "passed" | "failed";
44
+ message: string;
45
+ }
46
+ export interface StripeSandboxReadOnlyReport {
47
+ schemaVersion: "agentcert.sandbox_vendor_egress.v0.4";
48
+ kind: "agentcert.sandbox_vendor_egress";
49
+ implementation: "stripe-payment-intent-readonly";
50
+ vendor: "stripe";
51
+ environment: "sandbox";
52
+ generatedAt: string;
53
+ verdict: {
54
+ passed: boolean;
55
+ score: number;
56
+ };
57
+ summary: {
58
+ passed: number;
59
+ failed: number;
60
+ total: number;
61
+ };
62
+ checks: StripeSandboxReadOnlyCheck[];
63
+ policy: {
64
+ allowedOrigins: readonly [typeof STRIPE_TEST_API_ORIGIN];
65
+ allowedMethods: readonly ["GET"];
66
+ allowedResources: readonly [typeof STRIPE_PAYMENT_INTENT_RETRIEVE, typeof STRIPE_PAYMENT_INTENT_LIST];
67
+ timeoutMs: number;
68
+ maxRequestsPerMinute: number;
69
+ };
70
+ audit: VendorSandboxRequestAudit[];
71
+ observation?: StripePaymentIntentSnapshot;
72
+ disclaimer: string;
73
+ }
74
+ export interface RunStripeSandboxReadOnlyOptions extends StripeTestModeReadOnlyOptions {
75
+ paymentIntentId: string;
76
+ }
77
+ export declare function createStripeTestModeReadOnlyAdapter(options: StripeTestModeReadOnlyOptions): StripeTestModeReadOnlyAdapter;
78
+ export declare function runStripeSandboxReadOnlyCertification(options: RunStripeSandboxReadOnlyOptions): Promise<StripeSandboxReadOnlyReport>;
79
+ //# sourceMappingURL=stripe-test-readonly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe-test-readonly.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/stripe-test-readonly.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ import { createBoundedVendorSandboxEgress, VendorSandboxEgressError, } from "./vendor-sandbox-egress.js";
2
+ export const STRIPE_TEST_API_ORIGIN = "https://api.stripe.com";
3
+ export const STRIPE_SANDBOX_TIMEOUT_MS = 5_000;
4
+ export const STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE = 10;
5
+ export const STRIPE_PAYMENT_INTENT_RETRIEVE = "stripe.payment_intent.retrieve";
6
+ export const STRIPE_PAYMENT_INTENT_LIST = "stripe.payment_intent.list";
7
+ const STRIPE_RESOURCES = Object.freeze([
8
+ Object.freeze({
9
+ id: STRIPE_PAYMENT_INTENT_RETRIEVE,
10
+ method: "GET",
11
+ pathPattern: /^\/v1\/payment_intents\/pi_[A-Za-z0-9]{8,128}$/,
12
+ }),
13
+ Object.freeze({
14
+ id: STRIPE_PAYMENT_INTENT_LIST,
15
+ method: "GET",
16
+ pathPattern: /^\/v1\/payment_intents\?limit=(?:[1-9]|10)$/,
17
+ }),
18
+ ]);
19
+ export function createStripeTestModeReadOnlyAdapter(options) {
20
+ const apiKey = options.restrictedApiKey.trim();
21
+ if (!/^rk_test_[A-Za-z0-9_]{8,}$/.test(apiKey)) {
22
+ throw new Error("Stripe read-only reference adapter requires an rk_test_ restricted test-mode key.");
23
+ }
24
+ const timeoutMs = options.timeoutMs ?? STRIPE_SANDBOX_TIMEOUT_MS;
25
+ const maxRequestsPerMinute = options.maxRequestsPerMinute ?? STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE;
26
+ const egress = createBoundedVendorSandboxEgress({
27
+ policy: {
28
+ vendor: "stripe",
29
+ allowedOrigin: STRIPE_TEST_API_ORIGIN,
30
+ resources: STRIPE_RESOURCES,
31
+ timeoutMs,
32
+ maxRequestsPerMinute,
33
+ },
34
+ fetch: options.fetch,
35
+ now: options.now,
36
+ });
37
+ const get = (resource, path) => egress.requestJson({
38
+ resource,
39
+ method: "GET",
40
+ path,
41
+ headers: {
42
+ authorization: `Bearer ${apiKey}`,
43
+ accept: "application/json",
44
+ "user-agent": "agentcert-onegent-runtime/stripe-sandbox-readonly-v0.4",
45
+ },
46
+ });
47
+ return Object.freeze({
48
+ name: "stripe-test-mode-readonly",
49
+ safety: Object.freeze({
50
+ mode: "sandbox",
51
+ access: "read-only",
52
+ vendor: "stripe",
53
+ allowedOrigins: Object.freeze([STRIPE_TEST_API_ORIGIN]),
54
+ allowedMethods: Object.freeze(["GET"]),
55
+ allowedResources: Object.freeze([STRIPE_PAYMENT_INTENT_RETRIEVE, STRIPE_PAYMENT_INTENT_LIST]),
56
+ timeoutMs,
57
+ maxRequestsPerMinute,
58
+ }),
59
+ retrievePaymentIntent: async (id) => {
60
+ if (!/^pi_[A-Za-z0-9]{8,128}$/.test(id))
61
+ throw new Error("Stripe PaymentIntent ID is invalid.");
62
+ return paymentIntentSnapshot(await get(STRIPE_PAYMENT_INTENT_RETRIEVE, `/v1/payment_intents/${encodeURIComponent(id)}`));
63
+ },
64
+ listPaymentIntents: async (limit = 10) => {
65
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10)
66
+ throw new Error("Stripe read-only list limit must be an integer from 1 to 10.");
67
+ const payload = await get(STRIPE_PAYMENT_INTENT_LIST, `/v1/payment_intents?limit=${limit}`);
68
+ if (!isRecord(payload) || payload.object !== "list" || !Array.isArray(payload.data)) {
69
+ throw new Error("Stripe returned an invalid PaymentIntent list response.");
70
+ }
71
+ return payload.data.map(paymentIntentSnapshot);
72
+ },
73
+ getRequestAudit: () => egress.getAuditLog(),
74
+ });
75
+ }
76
+ export async function runStripeSandboxReadOnlyCertification(options) {
77
+ const generatedAt = new Date((options.now ?? Date.now)()).toISOString();
78
+ const timeoutMs = options.timeoutMs ?? STRIPE_SANDBOX_TIMEOUT_MS;
79
+ const maxRequestsPerMinute = options.maxRequestsPerMinute ?? STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE;
80
+ const checks = [];
81
+ let adapter;
82
+ let observation;
83
+ try {
84
+ adapter = createStripeTestModeReadOnlyAdapter(options);
85
+ checks.push(passed("restricted-test-key", "Credential format is restricted to Stripe rk_test_ keys and is not retained."), passed("fixed-egress-policy", "Egress is fixed to Stripe HTTPS, GET, and allowlisted PaymentIntent resources."));
86
+ observation = await adapter.retrievePaymentIntent(options.paymentIntentId);
87
+ checks.push(passed("bounded-read", "The allowlisted PaymentIntent read completed within the configured bounds."), passed("test-mode-response", "Stripe explicitly returned livemode=false."), passed("evidence-redaction", "The evidence snapshot excludes credentials, client_secret, metadata, and raw response bytes."));
88
+ }
89
+ catch (error) {
90
+ if (!adapter) {
91
+ checks.push(failed("restricted-test-key", "A valid Stripe rk_test_ restricted test-mode key was not available."), failed("fixed-egress-policy", "The request was not attempted because credential validation failed."));
92
+ }
93
+ else {
94
+ checks.push(failed("bounded-read", safeStripeFailure(error)));
95
+ }
96
+ checks.push(failed("test-mode-response", "No validated Stripe test-mode observation was produced."), failed("evidence-redaction", "No successful redacted observation was produced."));
97
+ }
98
+ const passedCount = checks.filter((check) => check.status === "passed").length;
99
+ const report = {
100
+ schemaVersion: "agentcert.sandbox_vendor_egress.v0.4",
101
+ kind: "agentcert.sandbox_vendor_egress",
102
+ implementation: "stripe-payment-intent-readonly",
103
+ vendor: "stripe",
104
+ environment: "sandbox",
105
+ generatedAt,
106
+ verdict: { passed: passedCount === checks.length, score: Math.round((passedCount / checks.length) * 100) },
107
+ summary: { passed: passedCount, failed: checks.length - passedCount, total: checks.length },
108
+ checks,
109
+ policy: {
110
+ allowedOrigins: [STRIPE_TEST_API_ORIGIN],
111
+ allowedMethods: ["GET"],
112
+ allowedResources: [STRIPE_PAYMENT_INTENT_RETRIEVE, STRIPE_PAYMENT_INTENT_LIST],
113
+ timeoutMs,
114
+ maxRequestsPerMinute,
115
+ },
116
+ audit: adapter?.getRequestAudit() ?? [],
117
+ ...(observation ? { observation } : {}),
118
+ disclaimer: "Stripe sandbox read-only evidence proves this bounded test-mode observation only. It does not authorize writes, attest Stripe controls, or certify production behavior.",
119
+ };
120
+ return structuredClone(report);
121
+ }
122
+ function paymentIntentSnapshot(value) {
123
+ if (!isRecord(value) || value.object !== "payment_intent" || typeof value.id !== "string") {
124
+ throw new Error("Stripe returned an invalid PaymentIntent response.");
125
+ }
126
+ if (value.livemode !== false)
127
+ throw new Error("Stripe read-only adapter refuses responses that are not explicitly test mode.");
128
+ return {
129
+ id: value.id,
130
+ object: "payment_intent",
131
+ livemode: false,
132
+ ...numberField(value, "amount", "amount"),
133
+ ...numberField(value, "amount_capturable", "amountCapturable"),
134
+ ...numberField(value, "amount_received", "amountReceived"),
135
+ ...stringField(value, "currency", "currency"),
136
+ ...stringField(value, "status", "status"),
137
+ ...numberField(value, "created", "created"),
138
+ };
139
+ }
140
+ function safeStripeFailure(error) {
141
+ if (error instanceof VendorSandboxEgressError)
142
+ return `Stripe sandbox read failed (${error.code}).`;
143
+ if (error instanceof Error && error.message.includes("not explicitly test mode"))
144
+ return "Stripe response was not explicitly test mode.";
145
+ if (error instanceof Error && error.message.includes("invalid PaymentIntent"))
146
+ return "Stripe returned an invalid PaymentIntent response.";
147
+ return "Stripe sandbox read failed before a validated observation was produced.";
148
+ }
149
+ function passed(id, message) {
150
+ return { id, status: "passed", message };
151
+ }
152
+ function failed(id, message) {
153
+ return { id, status: "failed", message };
154
+ }
155
+ function numberField(value, source, target) {
156
+ return typeof value[source] === "number" && Number.isFinite(value[source]) ? { [target]: value[source] } : {};
157
+ }
158
+ function stringField(value, source, target) {
159
+ return typeof value[source] === "string" ? { [target]: value[source] } : {};
160
+ }
161
+ function isRecord(value) {
162
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
163
+ }
@@ -0,0 +1,49 @@
1
+ export type VendorSandboxHttpMethod = "GET";
2
+ export interface VendorSandboxResourceRule {
3
+ id: string;
4
+ method: VendorSandboxHttpMethod;
5
+ pathPattern: RegExp;
6
+ }
7
+ export interface VendorSandboxEgressPolicy {
8
+ vendor: string;
9
+ allowedOrigin: string;
10
+ resources: readonly VendorSandboxResourceRule[];
11
+ timeoutMs: number;
12
+ maxRequestsPerMinute: number;
13
+ }
14
+ export type VendorSandboxAuditOutcome = "allowed" | "denied" | "rate_limited" | "timeout" | "http_error" | "failed";
15
+ export interface VendorSandboxRequestAudit {
16
+ requestId: string;
17
+ timestamp: string;
18
+ vendor: string;
19
+ resource: string;
20
+ method: VendorSandboxHttpMethod;
21
+ origin: string;
22
+ outcome: VendorSandboxAuditOutcome;
23
+ durationMs: number;
24
+ status?: number;
25
+ errorCode?: VendorSandboxEgressErrorCode;
26
+ }
27
+ export type VendorSandboxEgressErrorCode = "ORIGIN_DENIED" | "METHOD_DENIED" | "RESOURCE_DENIED" | "RATE_LIMITED" | "TIMEOUT" | "HTTP_ERROR" | "REQUEST_FAILED";
28
+ export declare class VendorSandboxEgressError extends Error {
29
+ readonly code: VendorSandboxEgressErrorCode;
30
+ readonly status?: number | undefined;
31
+ constructor(code: VendorSandboxEgressErrorCode, message: string, status?: number | undefined);
32
+ }
33
+ export interface VendorSandboxEgressRequest {
34
+ resource: string;
35
+ method: VendorSandboxHttpMethod;
36
+ path: string;
37
+ headers?: HeadersInit;
38
+ }
39
+ export interface BoundedVendorSandboxEgress {
40
+ requestJson(request: VendorSandboxEgressRequest): Promise<unknown>;
41
+ getAuditLog(): VendorSandboxRequestAudit[];
42
+ }
43
+ export interface BoundedVendorSandboxEgressOptions {
44
+ policy: VendorSandboxEgressPolicy;
45
+ fetch?: typeof fetch;
46
+ now?: () => number;
47
+ }
48
+ export declare function createBoundedVendorSandboxEgress(options: BoundedVendorSandboxEgressOptions): BoundedVendorSandboxEgress;
49
+ //# sourceMappingURL=vendor-sandbox-egress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vendor-sandbox-egress.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/vendor-sandbox-egress.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,yBAAyB,GACjC,SAAS,GACT,QAAQ,GACR,cAAc,GACd,SAAS,GACT,YAAY,GACZ,QAAQ,CAAC;AAEb,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,yBAAyB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,4BAA4B,CAAC;CAC1C;AAED,MAAM,MAAM,4BAA4B,GACpC,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,SAAS,GACT,YAAY,GACZ,gBAAgB,CAAC;AAErB,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,IAAI,EAAE,4BAA4B;aAElC,MAAM,CAAC,EAAE,MAAM;gBAFf,IAAI,EAAE,4BAA4B,EAClD,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA;CAKlC;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,WAAW,IAAI,yBAAyB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,iCAAiC;IAChD,MAAM,EAAE,yBAAyB,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAID,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CA+G5B"}
@@ -0,0 +1,152 @@
1
+ export class VendorSandboxEgressError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, message, status) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ this.name = "VendorSandboxEgressError";
9
+ }
10
+ }
11
+ const MAX_AUDIT_ENTRIES = 100;
12
+ export function createBoundedVendorSandboxEgress(options) {
13
+ const policy = validatePolicy(options.policy);
14
+ const requestFetch = options.fetch ?? fetch;
15
+ const now = options.now ?? Date.now;
16
+ const audit = [];
17
+ let requestSequence = 0;
18
+ let windowStartedAt = now();
19
+ let requestsInWindow = 0;
20
+ const record = (entry) => {
21
+ audit.push(Object.freeze({ ...entry }));
22
+ if (audit.length > MAX_AUDIT_ENTRIES)
23
+ audit.splice(0, audit.length - MAX_AUDIT_ENTRIES);
24
+ };
25
+ return Object.freeze({
26
+ requestJson: async (request) => {
27
+ const startedAt = now();
28
+ const requestId = `${policy.vendor}-${++requestSequence}`;
29
+ const method = request.method;
30
+ const url = safeUrl(request.path, policy.allowedOrigin);
31
+ const baseAudit = {
32
+ requestId,
33
+ timestamp: new Date(startedAt).toISOString(),
34
+ vendor: policy.vendor,
35
+ resource: request.resource,
36
+ method,
37
+ origin: url?.origin ?? policy.allowedOrigin,
38
+ };
39
+ const deny = (error) => {
40
+ record({ ...baseAudit, outcome: "denied", durationMs: elapsed(now, startedAt), errorCode: error.code });
41
+ throw error;
42
+ };
43
+ if (!url || url.origin !== policy.allowedOrigin || url.protocol !== "https:" || url.username || url.password || url.hash) {
44
+ return deny(new VendorSandboxEgressError("ORIGIN_DENIED", "Vendor sandbox request origin is not allowlisted."));
45
+ }
46
+ const resource = policy.resources.find((entry) => entry.id === request.resource);
47
+ if (!resource) {
48
+ return deny(new VendorSandboxEgressError("RESOURCE_DENIED", "Vendor sandbox resource is not allowlisted."));
49
+ }
50
+ if (method !== resource.method) {
51
+ return deny(new VendorSandboxEgressError("METHOD_DENIED", "Vendor sandbox HTTP method is not allowlisted for this resource."));
52
+ }
53
+ resource.pathPattern.lastIndex = 0;
54
+ if (!resource.pathPattern.test(`${url.pathname}${url.search}`)) {
55
+ return deny(new VendorSandboxEgressError("RESOURCE_DENIED", "Vendor sandbox path is not allowlisted for this resource."));
56
+ }
57
+ const current = now();
58
+ if (current - windowStartedAt >= 60_000) {
59
+ windowStartedAt = current;
60
+ requestsInWindow = 0;
61
+ }
62
+ if (requestsInWindow >= policy.maxRequestsPerMinute) {
63
+ const error = new VendorSandboxEgressError("RATE_LIMITED", "Vendor sandbox request limit exceeded. Retry after the current one-minute window.");
64
+ record({ ...baseAudit, outcome: "rate_limited", durationMs: elapsed(now, startedAt), errorCode: error.code });
65
+ throw error;
66
+ }
67
+ requestsInWindow += 1;
68
+ const controller = new AbortController();
69
+ let timedOut = false;
70
+ let timeout;
71
+ const operation = (async () => {
72
+ const response = await requestFetch(url, {
73
+ method,
74
+ headers: request.headers,
75
+ redirect: "error",
76
+ signal: controller.signal,
77
+ });
78
+ if (!response.ok) {
79
+ throw new VendorSandboxEgressError("HTTP_ERROR", `Vendor sandbox returned HTTP ${response.status}.`, response.status);
80
+ }
81
+ return { payload: await response.json(), status: response.status };
82
+ })();
83
+ const timeoutOperation = new Promise((_, reject) => {
84
+ timeout = setTimeout(() => {
85
+ timedOut = true;
86
+ controller.abort();
87
+ reject(new VendorSandboxEgressError("TIMEOUT", `Vendor sandbox request exceeded ${policy.timeoutMs} ms.`));
88
+ }, policy.timeoutMs);
89
+ });
90
+ try {
91
+ const result = await Promise.race([operation, timeoutOperation]);
92
+ record({ ...baseAudit, outcome: "allowed", durationMs: elapsed(now, startedAt), status: result.status });
93
+ return result.payload;
94
+ }
95
+ catch (cause) {
96
+ const error = cause instanceof VendorSandboxEgressError
97
+ ? cause
98
+ : timedOut
99
+ ? new VendorSandboxEgressError("TIMEOUT", `Vendor sandbox request exceeded ${policy.timeoutMs} ms.`)
100
+ : new VendorSandboxEgressError("REQUEST_FAILED", "Vendor sandbox request failed.");
101
+ record({
102
+ ...baseAudit,
103
+ outcome: error.code === "TIMEOUT" ? "timeout" : error.code === "HTTP_ERROR" ? "http_error" : "failed",
104
+ durationMs: elapsed(now, startedAt),
105
+ status: error.status,
106
+ errorCode: error.code,
107
+ });
108
+ throw error;
109
+ }
110
+ finally {
111
+ if (timeout)
112
+ clearTimeout(timeout);
113
+ }
114
+ },
115
+ getAuditLog: () => audit.map((entry) => ({ ...entry })),
116
+ });
117
+ }
118
+ function validatePolicy(policy) {
119
+ const origin = new URL(policy.allowedOrigin);
120
+ if (origin.protocol !== "https:" || origin.origin !== policy.allowedOrigin || origin.pathname !== "/" || origin.search || origin.hash) {
121
+ throw new Error("Vendor sandbox allowedOrigin must be an exact HTTPS origin without a path.");
122
+ }
123
+ if (!policy.vendor.trim())
124
+ throw new Error("Vendor sandbox policy vendor is required.");
125
+ if (!Number.isSafeInteger(policy.timeoutMs) || policy.timeoutMs < 100 || policy.timeoutMs > 30_000) {
126
+ throw new Error("Vendor sandbox timeoutMs must be an integer from 100 to 30000.");
127
+ }
128
+ if (!Number.isSafeInteger(policy.maxRequestsPerMinute) || policy.maxRequestsPerMinute < 1 || policy.maxRequestsPerMinute > 60) {
129
+ throw new Error("Vendor sandbox maxRequestsPerMinute must be an integer from 1 to 60.");
130
+ }
131
+ if (policy.resources.length === 0 || new Set(policy.resources.map((entry) => entry.id)).size !== policy.resources.length) {
132
+ throw new Error("Vendor sandbox policy requires unique resource rules.");
133
+ }
134
+ return Object.freeze({
135
+ ...policy,
136
+ resources: Object.freeze(policy.resources.map((entry) => Object.freeze({
137
+ ...entry,
138
+ pathPattern: new RegExp(entry.pathPattern.source, entry.pathPattern.flags),
139
+ }))),
140
+ });
141
+ }
142
+ function safeUrl(path, origin) {
143
+ try {
144
+ return new URL(path, `${origin}/`);
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ }
150
+ function elapsed(now, startedAt) {
151
+ return Math.max(0, Math.round(now() - startedAt));
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcert",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Assurance release gates, runtime evidence, and regression CI for AI agents.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",