witnora 0.8.1 → 0.9.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 +275 -12
- package/dist/artifact-manifest.js +37 -0
- package/dist/artifact-validation.js +102 -0
- package/dist/badge.js +31 -0
- package/dist/browser-adapter.js +156 -0
- package/dist/bundle.js +100 -0
- package/dist/cli.js +971 -0
- package/dist/command-help.js +71 -0
- package/dist/companion-artifacts.js +170 -0
- package/dist/conformance.js +115 -0
- package/dist/control-plane.js +409 -0
- package/dist/corpus-governance.js +126 -0
- package/dist/corpus-store.js +239 -0
- package/dist/corpus.js +438 -0
- package/dist/credentials.js +127 -0
- package/dist/evidence-signing.js +85 -0
- package/dist/failure-review.js +188 -0
- package/dist/guided-setup.js +101 -0
- package/dist/index.js +16 -0
- package/dist/lab.js +323 -0
- package/dist/local-server.js +491 -0
- package/dist/monitor.js +100 -0
- package/dist/normalizers.js +265 -0
- package/dist/onboarding-templates.js +158 -0
- package/dist/release-gate.js +457 -0
- package/dist/report.js +165 -0
- package/dist/runner.js +349 -0
- package/dist/sandbox.js +424 -0
- package/dist/schema-validator.js +503 -0
- package/dist/try.js +135 -0
- package/dist/types.js +3 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -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 +152 -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 +423 -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/trust-crypto.d.ts +11 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-types.js +7 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
- package/dist/vendor/onegent-runtime/types.d.ts +287 -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 +33 -13
- package/bin/witnora.js +0 -3
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { createPublicKey, randomBytes, randomUUID, verify } from "node:crypto";
|
|
2
|
+
import { canonicalJson, sha256, signDigest } from "./trust-crypto.js";
|
|
3
|
+
import { isRegisteredControlledActionAdapter, isRegisteredIndependentOutcomeProbe } from "./controlled-adapter.js";
|
|
4
|
+
export const BROWSER_ENFORCEMENT_PROTOCOL = "agentcert.browser_enforcement.v0.2";
|
|
5
|
+
export const EXECUTION_GRANT_CONTEXT = "agentcert.execution-grant.v0.2";
|
|
6
|
+
export const RUNTIME_CLAIM_CONTEXT = "onegent.runtime-claim.v0.2";
|
|
7
|
+
export const EXECUTION_SESSION_CONTEXT = "onegent.execution-session.v0.2";
|
|
8
|
+
export const EVENT_CONTEXT = "onegent.execution-event.v0.2";
|
|
9
|
+
export const EVENT_CHECKPOINT_CONTEXT = "onegent.event-checkpoint.v0.2";
|
|
10
|
+
export const OUTCOME_CONTEXT = "onegent.outcome-attestation.v0.2";
|
|
11
|
+
export const RECONCILIATION_CONTEXT = "onegent.reconciliation-report.v0.2";
|
|
12
|
+
export async function executeBrowserEnforcement(options) {
|
|
13
|
+
const now = options.now ?? (() => new Date());
|
|
14
|
+
verifyHostedExecutionGrant(options.grant, options.hostedIssuerKeys, now(), options.clockSkewMs ?? 30_000);
|
|
15
|
+
assertBindings(options);
|
|
16
|
+
if (!isRegisteredControlledActionAdapter(options.adapter))
|
|
17
|
+
throw new Error("ADAPTER_UNREGISTERED");
|
|
18
|
+
if (!isRegisteredIndependentOutcomeProbe(options.outcomeProbe))
|
|
19
|
+
throw new Error("OUTCOME_PROBE_UNREGISTERED");
|
|
20
|
+
if (options.outcomeProbe.name === options.adapter.name)
|
|
21
|
+
throw new Error("OUTCOME_NOT_INDEPENDENT");
|
|
22
|
+
const grant = options.grant.payload;
|
|
23
|
+
const executionSessionId = randomUUID();
|
|
24
|
+
const runtimeClaim = signRuntimeObject({
|
|
25
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
26
|
+
objectType: "RuntimeClaim",
|
|
27
|
+
signatureContext: RUNTIME_CLAIM_CONTEXT,
|
|
28
|
+
runtimeIdentityId: options.runtime.runtimeIdentityId,
|
|
29
|
+
executionGrantId: grant.executionGrantId,
|
|
30
|
+
executionGrantDigest: options.grant.payloadSha256,
|
|
31
|
+
actionId: grant.actionId,
|
|
32
|
+
principalIdentityAssertionId: grant.principalIdentityAssertionId,
|
|
33
|
+
principalIdentityDigest: grant.principalIdentityDigest,
|
|
34
|
+
principalIdentityProvider: grant.principalIdentityProvider,
|
|
35
|
+
executionSessionId,
|
|
36
|
+
claimNonce: randomBytes(24).toString("base64url"),
|
|
37
|
+
claimedAt: now().toISOString(),
|
|
38
|
+
runtimeKeyId: options.runtime.runtimeKeyId,
|
|
39
|
+
idempotencyKey: `claim:${grant.jti}`,
|
|
40
|
+
}, options.runtime.signer);
|
|
41
|
+
await options.claimGrant(runtimeClaim);
|
|
42
|
+
const credentialLeaseId = options.credentialLease.credentialLeaseId ?? randomUUID();
|
|
43
|
+
const startedAt = now().toISOString();
|
|
44
|
+
const genesis = sha256(canonicalJson({ executionSessionId, executionGrantDigest: options.grant.payloadSha256, runtimeIdentityId: options.runtime.runtimeIdentityId }));
|
|
45
|
+
const session = signRuntimeObject({
|
|
46
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
47
|
+
objectType: "ExecutionSessionAttestation",
|
|
48
|
+
signatureContext: EXECUTION_SESSION_CONTEXT,
|
|
49
|
+
executionSessionId,
|
|
50
|
+
executionGrantId: grant.executionGrantId,
|
|
51
|
+
executionGrantDigest: options.grant.payloadSha256,
|
|
52
|
+
actionId: grant.actionId,
|
|
53
|
+
actionIntentDigest: grant.actionIntentDigest,
|
|
54
|
+
runtimeIdentityId: options.runtime.runtimeIdentityId,
|
|
55
|
+
runtimeKeyId: options.runtime.runtimeKeyId,
|
|
56
|
+
adapterId: grant.adapterId,
|
|
57
|
+
adapterVersion: options.adapterVersion,
|
|
58
|
+
browserContextIdDigest: sha256(`browser-context:${executionSessionId}`),
|
|
59
|
+
credentialLeaseId,
|
|
60
|
+
credentialIsolationMode: options.credentialLease.isolationMode,
|
|
61
|
+
targetAudience: grant.targetAudience,
|
|
62
|
+
allowedOrigins: [...grant.allowedOrigins],
|
|
63
|
+
startedAt,
|
|
64
|
+
sessionExpiresAt: grant.expiresAt,
|
|
65
|
+
eventChainGenesisHash: genesis,
|
|
66
|
+
}, options.runtime.signer);
|
|
67
|
+
const events = [];
|
|
68
|
+
let previousHash = genesis;
|
|
69
|
+
const append = async (eventType, payload = {}) => {
|
|
70
|
+
const redactedPayload = redactAndBound(payload);
|
|
71
|
+
const event = {
|
|
72
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
73
|
+
objectType: "ExecutionEvent",
|
|
74
|
+
signatureContext: EVENT_CONTEXT,
|
|
75
|
+
eventId: randomUUID(),
|
|
76
|
+
actionId: grant.actionId,
|
|
77
|
+
executionSessionId,
|
|
78
|
+
sequence: events.length + 1,
|
|
79
|
+
previousEventHash: previousHash,
|
|
80
|
+
eventType,
|
|
81
|
+
sourceIdentityId: options.runtime.runtimeIdentityId,
|
|
82
|
+
sourceTimestamp: now().toISOString(),
|
|
83
|
+
receivedTimestamp: now().toISOString(),
|
|
84
|
+
payloadDigest: sha256(canonicalJson(redactedPayload)),
|
|
85
|
+
redactedPayload,
|
|
86
|
+
};
|
|
87
|
+
const signed = signRuntimeObject(event, options.runtime.signer);
|
|
88
|
+
previousHash = signed.payloadSha256;
|
|
89
|
+
events.push(signed);
|
|
90
|
+
await options.recorder.append(eventType, { actionId: grant.actionId, executionSessionId, ...redactedPayload });
|
|
91
|
+
};
|
|
92
|
+
const leaseIssuedAt = now().toISOString();
|
|
93
|
+
const providerReferenceDigest = sha256(options.credentialLease.providerReference);
|
|
94
|
+
const credentialLease = {
|
|
95
|
+
credentialLeaseId,
|
|
96
|
+
tenantId: grant.tenantId,
|
|
97
|
+
actionId: grant.actionId,
|
|
98
|
+
executionSessionId,
|
|
99
|
+
providerType: options.credentialLease.providerType,
|
|
100
|
+
providerReference: `opaque:${providerReferenceDigest}`,
|
|
101
|
+
targetAudience: grant.targetAudience,
|
|
102
|
+
scopeDigest: sha256(canonicalJson({ allowedOrigins: grant.allowedOrigins, operation: grant.allowedOperation, resource: grant.allowedResource })),
|
|
103
|
+
isolationMode: options.credentialLease.isolationMode,
|
|
104
|
+
issuedAt: leaseIssuedAt,
|
|
105
|
+
expiresAt: options.credentialLease.expiresAt,
|
|
106
|
+
status: "ACTIVE",
|
|
107
|
+
};
|
|
108
|
+
let leaseRevoked = false;
|
|
109
|
+
const revokeCredentialLease = async () => {
|
|
110
|
+
if (leaseRevoked)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
await options.credentialLease.revoke?.();
|
|
114
|
+
credentialLease.status = "REVOKED";
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
credentialLease.status = "FAILED";
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
const revokedAt = now().toISOString();
|
|
122
|
+
credentialLease.revokedAt = revokedAt;
|
|
123
|
+
leaseRevoked = true;
|
|
124
|
+
await append("CREDENTIAL_LEASE_REVOKED", { credentialLeaseId, revokedAt, status: credentialLease.status });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
await append("EXECUTION_GRANT_VERIFIED", { executionGrantDigest: options.grant.payloadSha256 });
|
|
129
|
+
await append("EXECUTION_GRANT_CLAIMED", { executionGrantId: grant.executionGrantId, claimDigest: runtimeClaim.payloadSha256 });
|
|
130
|
+
await append("EXECUTION_SESSION_STARTED", { executionSessionDigest: session.payloadSha256 });
|
|
131
|
+
await append("CREDENTIAL_LEASE_ACTIVATED", { credentialLeaseId, providerReferenceDigest, isolationMode: options.credentialLease.isolationMode });
|
|
132
|
+
await append("BROWSER_CONTEXT_CREATED", { browserContextIdDigest: session.payload.browserContextIdDigest });
|
|
133
|
+
await append("TARGET_NAVIGATION", { targetAudience: grant.targetAudience, origin: grant.allowedOrigins[0] });
|
|
134
|
+
await append("AUTHORIZED_ACTION_PREPARED", { operation: grant.allowedOperation, resource: grant.allowedResource });
|
|
135
|
+
const finalParametersDigest = sha256(canonicalJson(options.approvedParameters));
|
|
136
|
+
if (finalParametersDigest !== grant.parametersDigest) {
|
|
137
|
+
throw new Error("PARAMETERS_DIGEST_MISMATCH");
|
|
138
|
+
}
|
|
139
|
+
await append("FINAL_PARAMETERS_VERIFIED", { parametersDigest: finalParametersDigest });
|
|
140
|
+
await append("HIGH_RISK_SUBMISSION_STARTED", { actionId: grant.actionId });
|
|
141
|
+
const adapterResult = await options.adapter.execute(options.action, { idempotencyKey: executionSessionId, attempt: 1 });
|
|
142
|
+
const execution = {
|
|
143
|
+
method: adapterResult.method ?? "LOCAL_ADAPTER",
|
|
144
|
+
status: "COMPLETED",
|
|
145
|
+
targetSystem: adapterResult.targetSystem ?? options.action.targetSystem,
|
|
146
|
+
previousState: adapterResult.previousState,
|
|
147
|
+
observedState: adapterResult.observedState,
|
|
148
|
+
rollbackToken: adapterResult.rollbackToken,
|
|
149
|
+
};
|
|
150
|
+
await options.markConsumed(grant.executionGrantId, executionSessionId);
|
|
151
|
+
await append("HIGH_RISK_SUBMISSION_COMPLETED", { targetSystem: execution.targetSystem });
|
|
152
|
+
await append("TARGET_RESPONSE_OBSERVED", { observedStateDigest: sha256(canonicalJson(execution.observedState ?? {})) });
|
|
153
|
+
await append("OUTCOME_PROBE_STARTED", { probe: options.outcomeProbe.name });
|
|
154
|
+
const observation = await options.outcomeProbe.observe(options.action, execution);
|
|
155
|
+
await append("OUTCOME_OBSERVED", { observationId: observation.observationId, source: observation.source, observedStateDigest: sha256(canonicalJson(observation.observedState)) });
|
|
156
|
+
const predicateDigest = sha256(canonicalJson(options.outcomePredicate));
|
|
157
|
+
const expectedDigest = sha256(canonicalJson(options.action.proposedAfterState));
|
|
158
|
+
const observedDigest = sha256(canonicalJson(observation.observedState));
|
|
159
|
+
const outcomeResult = subsetMatches(options.action.proposedAfterState, observation.observedState) ? "SATISFIED" : "NOT_SATISFIED";
|
|
160
|
+
const outcomeAttestation = signRuntimeObject({
|
|
161
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
162
|
+
objectType: "OutcomeAttestation",
|
|
163
|
+
signatureContext: OUTCOME_CONTEXT,
|
|
164
|
+
outcomeAttestationId: randomUUID(),
|
|
165
|
+
actionId: grant.actionId,
|
|
166
|
+
executionSessionId,
|
|
167
|
+
executionGrantId: grant.executionGrantId,
|
|
168
|
+
predicateId: "expected_state_subset",
|
|
169
|
+
predicateVersion: "1",
|
|
170
|
+
predicateDigest,
|
|
171
|
+
expectedStateDigest: expectedDigest,
|
|
172
|
+
observedStateDigest: observedDigest,
|
|
173
|
+
result: outcomeResult,
|
|
174
|
+
observationMethod: "TARGET_AUDIT_LOG",
|
|
175
|
+
observationIndependence: "SEPARATE_READ_ONLY_PROBE",
|
|
176
|
+
observationSource: observation.source,
|
|
177
|
+
sourceEvidenceDigest: sha256(canonicalJson(observation)),
|
|
178
|
+
evidenceReferences: [`target-audit:${observation.observationId}`],
|
|
179
|
+
collectorIdentityId: options.runtime.runtimeIdentityId,
|
|
180
|
+
collectorKeyId: options.runtime.runtimeKeyId,
|
|
181
|
+
collectedAt: observation.observedAt,
|
|
182
|
+
confidence: 1,
|
|
183
|
+
limitations: ["The reference probe reads a dedicated sandbox audit source; it is not hardware attested."],
|
|
184
|
+
}, options.runtime.signer);
|
|
185
|
+
await revokeCredentialLease();
|
|
186
|
+
await append("EXECUTION_SESSION_COMPLETED", { outcomeAttestationDigest: outcomeAttestation.payloadSha256 });
|
|
187
|
+
const auditEvents = await options.targetAuditSource.listEvents(startedAt, now().toISOString());
|
|
188
|
+
const matching = auditEvents.filter((event) => event.actionId === grant.actionId && event.executionSessionId === executionSessionId && event.operation === grant.allowedOperation && event.resource === grant.allowedResource && event.parametersDigest === grant.parametersDigest);
|
|
189
|
+
const unmatchedTargetEvents = auditEvents.filter((event) => !matching.includes(event)).map((event) => event.targetEventId);
|
|
190
|
+
const duplicateMatches = matching.length > 1 ? matching.slice(1).map((event) => event.targetEventId) : [];
|
|
191
|
+
const reconciliationResult = options.targetAuditSource.exclusiveCredential && matching.length === 1 && !unmatchedTargetEvents.length && !duplicateMatches.length ? "PASSED" : unmatchedTargetEvents.length || duplicateMatches.length ? "BYPASS_DETECTED" : "INCOMPLETE";
|
|
192
|
+
const reconciliationReport = signRuntimeObject({
|
|
193
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
194
|
+
objectType: "ReconciliationReport",
|
|
195
|
+
signatureContext: RECONCILIATION_CONTEXT,
|
|
196
|
+
reconciliationReportId: randomUUID(),
|
|
197
|
+
tenantId: grant.tenantId,
|
|
198
|
+
actionId: grant.actionId,
|
|
199
|
+
executionSessionId,
|
|
200
|
+
targetSystem: grant.targetAudience,
|
|
201
|
+
accountOrCredentialReferenceDigest: providerReferenceDigest,
|
|
202
|
+
reconciliationWindowStart: startedAt,
|
|
203
|
+
reconciliationWindowEnd: now().toISOString(),
|
|
204
|
+
expectedActionIds: [grant.actionId],
|
|
205
|
+
observedTargetEvents: auditEvents,
|
|
206
|
+
matchedEvents: matching.map((event) => event.targetEventId),
|
|
207
|
+
unmatchedTargetEvents,
|
|
208
|
+
unmatchedReceipts: matching.length ? [] : [grant.actionId],
|
|
209
|
+
duplicateMatches,
|
|
210
|
+
result: reconciliationResult,
|
|
211
|
+
limitations: ["Reconciliation covers the dedicated sandbox credential and declared time window only."],
|
|
212
|
+
collectorIdentityId: options.runtime.runtimeIdentityId,
|
|
213
|
+
collectedAt: now().toISOString(),
|
|
214
|
+
}, options.runtime.signer);
|
|
215
|
+
const eventCheckpoint = signRuntimeObject({
|
|
216
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
217
|
+
objectType: "EventChainCheckpoint",
|
|
218
|
+
signatureContext: EVENT_CHECKPOINT_CONTEXT,
|
|
219
|
+
actionId: grant.actionId,
|
|
220
|
+
executionSessionId,
|
|
221
|
+
eventCount: events.length,
|
|
222
|
+
firstEventHash: events[0].payloadSha256,
|
|
223
|
+
finalEventHash: events.at(-1).payloadSha256,
|
|
224
|
+
completedAt: now().toISOString(),
|
|
225
|
+
}, options.runtime.signer);
|
|
226
|
+
return {
|
|
227
|
+
protocolVersion: BROWSER_ENFORCEMENT_PROTOCOL,
|
|
228
|
+
executionGrant: options.grant,
|
|
229
|
+
grantStatus: "CONSUMED",
|
|
230
|
+
runtimeClaim,
|
|
231
|
+
executionSession: session,
|
|
232
|
+
credentialLease,
|
|
233
|
+
events,
|
|
234
|
+
eventCheckpoint,
|
|
235
|
+
outcomeAttestation,
|
|
236
|
+
reconciliationReport,
|
|
237
|
+
finalParametersDigest,
|
|
238
|
+
detectedBypass: reconciliationResult !== "PASSED",
|
|
239
|
+
execution,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
try {
|
|
244
|
+
await append("EXECUTION_SESSION_FAILED", { reasonCode: error instanceof Error ? error.message.slice(0, 200) : "EXECUTION_FAILED" });
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
if (!leaseRevoked)
|
|
248
|
+
await revokeCredentialLease();
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export function verifyHostedExecutionGrant(grant, issuerKeys, at = new Date(), clockSkewMs = 30_000) {
|
|
254
|
+
const payload = grant.payload;
|
|
255
|
+
if (payload.protocolVersion !== BROWSER_ENFORCEMENT_PROTOCOL || payload.objectType !== "ExecutionGrant" || payload.signatureContext !== EXECUTION_GRANT_CONTEXT)
|
|
256
|
+
throw new Error("UNSUPPORTED_PROTOCOL_VERSION");
|
|
257
|
+
if (payload.maxUses !== 1 || grant.payloadSha256 !== sha256(canonicalJson(payload)))
|
|
258
|
+
throw new Error("EXECUTION_GRANT_DIGEST_MISMATCH");
|
|
259
|
+
const publicKey = issuerKeys[payload.issuerKeyId];
|
|
260
|
+
if (!publicKey)
|
|
261
|
+
throw new Error("EXECUTION_GRANT_UNKNOWN_ISSUER");
|
|
262
|
+
const bytes = Buffer.from(canonicalJson(payload));
|
|
263
|
+
if (grant.attestation.payloadSha256 !== sha256(bytes))
|
|
264
|
+
throw new Error("EXECUTION_GRANT_INVALID_SIGNATURE");
|
|
265
|
+
let valid = false;
|
|
266
|
+
try {
|
|
267
|
+
valid = verify(null, bytes, createPublicKey(publicKey), Buffer.from(grant.attestation.signature, "base64url"));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
valid = false;
|
|
271
|
+
}
|
|
272
|
+
if (!valid)
|
|
273
|
+
throw new Error("EXECUTION_GRANT_INVALID_SIGNATURE");
|
|
274
|
+
if (at.getTime() + clockSkewMs < Date.parse(payload.notBefore))
|
|
275
|
+
throw new Error("EXECUTION_GRANT_NOT_YET_VALID");
|
|
276
|
+
if (at.getTime() - clockSkewMs >= Date.parse(payload.expiresAt))
|
|
277
|
+
throw new Error("EXECUTION_GRANT_EXPIRED");
|
|
278
|
+
}
|
|
279
|
+
export function signRuntimeObject(payload, signer) {
|
|
280
|
+
const payloadSha256 = sha256(canonicalJson(payload));
|
|
281
|
+
return { payload, payloadSha256, signature: signDigest(payloadSha256, signer) };
|
|
282
|
+
}
|
|
283
|
+
function assertBindings(options) {
|
|
284
|
+
const grant = options.grant.payload;
|
|
285
|
+
if (grant.expectedRuntimeIdentityId !== options.runtime.runtimeIdentityId)
|
|
286
|
+
throw new Error("RUNTIME_IDENTITY_MISMATCH");
|
|
287
|
+
if (grant.actionId !== options.action.id || grant.actionIntentDigest !== options.actionIntentDigest)
|
|
288
|
+
throw new Error("ACTION_DIGEST_MISMATCH");
|
|
289
|
+
if (grant.agentBuildDigest !== options.agentBuildDigest)
|
|
290
|
+
throw new Error("AGENT_BUILD_MISMATCH");
|
|
291
|
+
if (grant.adapterId !== options.adapter.name || grant.targetAudience !== options.action.targetSystem)
|
|
292
|
+
throw new Error("ADAPTER_MISMATCH");
|
|
293
|
+
if (!versionSatisfiesConstraint(options.adapterVersion, grant.adapterVersionConstraint))
|
|
294
|
+
throw new Error("ADAPTER_VERSION_MISMATCH");
|
|
295
|
+
if (!options.action.targetUrl || !grant.allowedOrigins.includes(normalizedOrigin(options.action.targetUrl)))
|
|
296
|
+
throw new Error("TARGET_ORIGIN_MISMATCH");
|
|
297
|
+
if (!options.adapter.control.allowedActionTypes.includes(options.action.actionType) || !options.adapter.control.allowedTargetSystems.includes(options.action.targetSystem))
|
|
298
|
+
throw new Error("ADAPTER_SCOPE_MISMATCH");
|
|
299
|
+
if (sha256(canonicalJson(options.outcomePredicate)) !== grant.outcomePredicateDigest)
|
|
300
|
+
throw new Error("OUTCOME_PREDICATE_MISMATCH");
|
|
301
|
+
}
|
|
302
|
+
function normalizedOrigin(value) {
|
|
303
|
+
try {
|
|
304
|
+
return new URL(value).origin;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
throw new Error("TARGET_ORIGIN_INVALID");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function versionSatisfiesConstraint(version, constraint) {
|
|
311
|
+
const parsed = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
312
|
+
if (!parsed)
|
|
313
|
+
return false;
|
|
314
|
+
const exact = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(constraint);
|
|
315
|
+
if (exact)
|
|
316
|
+
return parsed.slice(1).join(".") === exact.slice(1).join(".");
|
|
317
|
+
const compatible = /^\^v?(\d+)\.(\d+)\.(\d+)$/.exec(constraint);
|
|
318
|
+
if (!compatible)
|
|
319
|
+
return false;
|
|
320
|
+
const [major, minor, patch] = parsed.slice(1).map(Number);
|
|
321
|
+
const [requiredMajor, requiredMinor, requiredPatch] = compatible.slice(1).map(Number);
|
|
322
|
+
return major === requiredMajor && (minor > requiredMinor || (minor === requiredMinor && patch >= requiredPatch));
|
|
323
|
+
}
|
|
324
|
+
function redactAndBound(value) {
|
|
325
|
+
const sensitive = /authorization|cookie|password|secret|token|credential|api.?key/i;
|
|
326
|
+
const redact = (input, depth) => {
|
|
327
|
+
if (depth > 8)
|
|
328
|
+
return "[TRUNCATED]";
|
|
329
|
+
if (Array.isArray(input))
|
|
330
|
+
return input.slice(0, 100).map((item) => redact(item, depth + 1));
|
|
331
|
+
if (input && typeof input === "object")
|
|
332
|
+
return Object.fromEntries(Object.entries(input).slice(0, 100).map(([key, child]) => [key, sensitive.test(key) ? "[REDACTED]" : redact(child, depth + 1)]));
|
|
333
|
+
if (typeof input === "string")
|
|
334
|
+
return input.slice(0, 2_048);
|
|
335
|
+
return input;
|
|
336
|
+
};
|
|
337
|
+
const output = redact(value, 0);
|
|
338
|
+
if (Buffer.byteLength(canonicalJson(output)) > 32_768)
|
|
339
|
+
throw new Error("EXECUTION_EVENT_TOO_LARGE");
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
function subsetMatches(expected, observed) {
|
|
343
|
+
return Object.entries(expected).every(([key, value]) => canonicalJson(observed[key]) === canonicalJson(value));
|
|
344
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ControlledActionAdapter, IndependentOutcomeProbe } from "./trust-types.js";
|
|
2
|
+
export declare function createControlledActionAdapter(adapter: ControlledActionAdapter): ControlledActionAdapter;
|
|
3
|
+
export declare function isRegisteredControlledActionAdapter(adapter: ControlledActionAdapter): boolean;
|
|
4
|
+
export declare function createIndependentOutcomeProbe(probe: IndependentOutcomeProbe): IndependentOutcomeProbe;
|
|
5
|
+
export declare function isRegisteredIndependentOutcomeProbe(probe: IndependentOutcomeProbe): boolean;
|
|
6
|
+
//# sourceMappingURL=controlled-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controlled-adapter.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/controlled-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAKzF,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAYvG;AAED,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAE7F;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,uBAAuB,GAAG,uBAAuB,CAKrG;AAED,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAE3F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const registeredAdapters = new WeakSet();
|
|
2
|
+
const registeredProbes = new WeakSet();
|
|
3
|
+
export function createControlledActionAdapter(adapter) {
|
|
4
|
+
validateBoundary(adapter);
|
|
5
|
+
const controlled = Object.freeze({
|
|
6
|
+
...adapter,
|
|
7
|
+
control: Object.freeze({
|
|
8
|
+
...adapter.control,
|
|
9
|
+
allowedActionTypes: Object.freeze([...adapter.control.allowedActionTypes]),
|
|
10
|
+
allowedTargetSystems: Object.freeze([...adapter.control.allowedTargetSystems]),
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
registeredAdapters.add(controlled);
|
|
14
|
+
return controlled;
|
|
15
|
+
}
|
|
16
|
+
export function isRegisteredControlledActionAdapter(adapter) {
|
|
17
|
+
return registeredAdapters.has(adapter);
|
|
18
|
+
}
|
|
19
|
+
export function createIndependentOutcomeProbe(probe) {
|
|
20
|
+
if (!probe.name?.trim() || probe.independent !== true)
|
|
21
|
+
throw new Error("Independent outcome probe name and boundary are required.");
|
|
22
|
+
const controlled = Object.freeze({ ...probe, independent: true });
|
|
23
|
+
registeredProbes.add(controlled);
|
|
24
|
+
return controlled;
|
|
25
|
+
}
|
|
26
|
+
export function isRegisteredIndependentOutcomeProbe(probe) {
|
|
27
|
+
return registeredProbes.has(probe);
|
|
28
|
+
}
|
|
29
|
+
function validateBoundary(adapter) {
|
|
30
|
+
if (!adapter.name?.trim())
|
|
31
|
+
throw new Error("Controlled adapter name is required.");
|
|
32
|
+
const control = adapter.control;
|
|
33
|
+
if (!control || control.mode !== "agentcert_gateway" || control.credentials !== "gateway_managed"
|
|
34
|
+
|| control.bypassPrevention !== "credentials_unavailable_to_agent") {
|
|
35
|
+
throw new Error(`Adapter ${adapter.name} does not provide the required Witnora credential-isolated gateway boundary.`);
|
|
36
|
+
}
|
|
37
|
+
if (!control.allowedActionTypes.length || !control.allowedTargetSystems.length) {
|
|
38
|
+
throw new Error(`Adapter ${adapter.name} must declare bounded action types and target systems.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -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
|
+
}
|