@axtary/actionpass 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/package.json +35 -0
  3. package/src/index.ts +552 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @axtary/actionpass
2
+
3
+ Scoped, signed ActionPass artifacts for runtime-governed AI agent actions.
4
+
5
+ This is an early package reservation and v0 development build for Axtary. The API is not stable yet.
6
+
7
+ ## What It Does
8
+
9
+ - Validates normalized agent actions at runtime.
10
+ - Evaluates a first deterministic GitHub PR policy.
11
+ - Produces canonical SHA-256 payload hashes.
12
+ - Issues signed ActionPass JWT/JWS artifacts for allowed actions.
13
+ - Verifies that a signed pass still matches the exact action payload.
14
+ - Records ledger entries with hashable decision evidence.
15
+
16
+ ## Current Status
17
+
18
+ Version `0.0.1` is a placeholder/pre-release package. Do not use it for production authorization.
19
+
20
+ Before production use, Axtary still needs:
21
+
22
+ - A built `dist/` output instead of raw TypeScript exports.
23
+ - Stable schema versioning.
24
+ - Revocation and key rotation.
25
+ - Policy package extraction.
26
+ - External security review.
27
+
28
+ ## Example Shape
29
+
30
+ ```ts
31
+ import {
32
+ authorize,
33
+ demoAction,
34
+ verifyActionPass,
35
+ } from "@axtary/actionpass";
36
+
37
+ const result = await authorize({
38
+ action: demoAction,
39
+ issuer: "https://api.axtary.dev",
40
+ tenant: "org:company",
41
+ signingKey,
42
+ });
43
+
44
+ if (result.actionPass) {
45
+ const verified = await verifyActionPass({
46
+ token: result.actionPass.token,
47
+ action: demoAction,
48
+ verificationKey,
49
+ issuer: "https://api.axtary.dev",
50
+ });
51
+ }
52
+ ```
53
+
54
+ ## Security Notes
55
+
56
+ ActionPass is designed to fail closed:
57
+
58
+ - Malformed actions fail schema validation.
59
+ - Denied and step-up actions do not receive passes.
60
+ - Verification rejects expired tokens.
61
+ - Verification rejects payload hash mismatches.
62
+ - Verification binds agent, human owner, runtime, task, tool, resource, and payload hash.
63
+
64
+ Signing currently defaults to `ES256`.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@axtary/actionpass",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Scoped, signed ActionPass artifacts for runtime-governed AI agent actions.",
6
+ "type": "module",
7
+ "license": "UNLICENSED",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/axtary/axtary.git",
11
+ "directory": "packages/actionpass"
12
+ },
13
+ "keywords": [
14
+ "ai-agents",
15
+ "authorization",
16
+ "runtime-security",
17
+ "jwt",
18
+ "mcp"
19
+ ],
20
+ "engines": {
21
+ "node": "^20.19.0 || ^22.13.0 || >=24"
22
+ },
23
+ "files": [
24
+ "README.md",
25
+ "src/index.ts"
26
+ ],
27
+ "exports": {
28
+ ".": "./src/index.ts"
29
+ },
30
+ "types": "./src/index.ts",
31
+ "dependencies": {
32
+ "jose": "^6.2.3",
33
+ "zod": "^4.4.3"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,552 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { SignJWT, jwtVerify, type JWK, type KeyObject } from "jose";
3
+ import { z } from "zod";
4
+
5
+ export const ACTION_SCHEMA_VERSION = "axtary.action.v0";
6
+ export const ACTIONPASS_VERSION = "axtary.actionpass.v0";
7
+ export const LEDGER_SCHEMA_VERSION = "axtary.ledger.v0";
8
+ export const DEFAULT_EXPIRES_IN_SECONDS = 600;
9
+ export const DEFAULT_SIGNING_ALGORITHM = "ES256";
10
+
11
+ export type JsonValue =
12
+ | string
13
+ | number
14
+ | boolean
15
+ | null
16
+ | JsonValue[]
17
+ | { [key: string]: JsonValue };
18
+
19
+ const JsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
20
+ z.union([
21
+ z.string(),
22
+ z.number().finite(),
23
+ z.boolean(),
24
+ z.null(),
25
+ z.array(JsonValueSchema),
26
+ z.record(z.string(), JsonValueSchema),
27
+ ])
28
+ );
29
+
30
+ export const AxtaryDecisionSchema = z.enum(["allow", "deny", "step_up"]);
31
+ export type AxtaryDecision = z.infer<typeof AxtaryDecisionSchema>;
32
+
33
+ export const NormalizedActionSchema = z.object({
34
+ schemaVersion: z.literal(ACTION_SCHEMA_VERSION).default(ACTION_SCHEMA_VERSION),
35
+ actor: z.object({
36
+ agentId: z.string().min(1),
37
+ humanOwner: z.string().min(1),
38
+ runtime: z.string().min(1),
39
+ tenant: z.string().min(1).optional(),
40
+ }),
41
+ intent: z.object({
42
+ taskId: z.string().min(1),
43
+ declaredGoal: z.string().min(1),
44
+ maxDelegationDepth: z.number().int().nonnegative().optional(),
45
+ }),
46
+ capability: z.object({
47
+ tool: z.string().min(1),
48
+ resource: z.string().min(1),
49
+ payload: z.record(z.string(), JsonValueSchema),
50
+ constraints: z.record(z.string(), JsonValueSchema).optional(),
51
+ }),
52
+ toolDefinition: z
53
+ .object({
54
+ serverIdentity: z.string().min(1),
55
+ schemaVersion: z.string().min(1),
56
+ definitionHash: z.string().min(1),
57
+ })
58
+ .optional(),
59
+ budget: z
60
+ .object({
61
+ reservationId: z.string().min(1).optional(),
62
+ limit: z.record(z.string(), JsonValueSchema).optional(),
63
+ commitStatus: z.enum(["pending", "committed", "rolled_back"]).optional(),
64
+ })
65
+ .optional(),
66
+ });
67
+ export type NormalizedAction = z.infer<typeof NormalizedActionSchema>;
68
+
69
+ export const PolicyDecisionSchema = z.object({
70
+ decision: AxtaryDecisionSchema,
71
+ reasons: z.array(z.string().min(1)),
72
+ policy: z.object({
73
+ nativeRule: z.string().min(1),
74
+ version: z.string().min(1),
75
+ cedarCompatible: z.boolean(),
76
+ opaCompatible: z.boolean(),
77
+ }),
78
+ constraints: z.object({
79
+ expiresInSeconds: z.number().int().positive(),
80
+ maxFilesChanged: z.number().int().nonnegative(),
81
+ blockedPaths: z.array(z.string().min(1)),
82
+ }),
83
+ });
84
+ export type PolicyDecision = z.infer<typeof PolicyDecisionSchema>;
85
+ export type ActionPassDecision = PolicyDecision;
86
+
87
+ export const ApprovalSchema = z.object({
88
+ mode: z.enum(["none", "human", "policy_override"]),
89
+ approvedBy: z.string().min(1).optional(),
90
+ approvalArtifact: z.string().min(1).optional(),
91
+ approvedAt: z.string().datetime().optional(),
92
+ });
93
+ export type Approval = z.infer<typeof ApprovalSchema>;
94
+
95
+ export const ActionPassClaimsSchema = z.object({
96
+ apv: z.literal(ACTIONPASS_VERSION),
97
+ iss: z.string().min(1),
98
+ sub: z.string().min(1),
99
+ aud: z.union([z.string().min(1), z.array(z.string().min(1))]),
100
+ exp: z.number().int().positive(),
101
+ nbf: z.number().int().positive(),
102
+ iat: z.number().int().positive(),
103
+ jti: z.string().min(1),
104
+ tenant: z.string().min(1),
105
+ humanOwner: z.string().min(1),
106
+ agentRuntime: z.string().min(1),
107
+ intent: NormalizedActionSchema.shape.intent,
108
+ capability: z.object({
109
+ tool: z.string().min(1),
110
+ resource: z.string().min(1),
111
+ constraints: z.record(z.string(), JsonValueSchema).optional(),
112
+ }),
113
+ decision: AxtaryDecisionSchema,
114
+ reasons: z.array(z.string().min(1)),
115
+ policy: PolicyDecisionSchema.shape.policy,
116
+ payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
117
+ approval: ApprovalSchema.optional(),
118
+ audit: z.object({
119
+ traceId: z.string().min(1),
120
+ parentPassId: z.string().min(1).nullable(),
121
+ ledgerHash: z.string().min(1).nullable(),
122
+ }),
123
+ });
124
+ export type ActionPassClaims = z.infer<typeof ActionPassClaimsSchema>;
125
+
126
+ export const LedgerRecordSchema = z.object({
127
+ schemaVersion: z.literal(LEDGER_SCHEMA_VERSION),
128
+ id: z.string().min(1),
129
+ occurredAt: z.string().datetime(),
130
+ actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
131
+ payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
132
+ decision: AxtaryDecisionSchema,
133
+ reasons: z.array(z.string().min(1)),
134
+ policy: PolicyDecisionSchema.shape.policy,
135
+ actionPassId: z.string().min(1).nullable(),
136
+ previousLedgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(),
137
+ ledgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
138
+ });
139
+ export type LedgerRecord = z.infer<typeof LedgerRecordSchema>;
140
+
141
+ export type SigningKey = CryptoKey | KeyObject | JWK | Uint8Array;
142
+ export type VerificationKey = CryptoKey | KeyObject | JWK | Uint8Array;
143
+ export type SigningAlgorithm = "ES256" | "EdDSA";
144
+
145
+ export type PolicyOptions = {
146
+ allowedTool?: string;
147
+ requiredBaseBranch?: string;
148
+ maxFilesChanged?: number;
149
+ blockedPathPrefixes?: string[];
150
+ policyVersion?: string;
151
+ };
152
+
153
+ export type IssueActionPassInput = {
154
+ action: unknown;
155
+ decision: PolicyDecision;
156
+ issuer: string;
157
+ tenant: string;
158
+ signingKey: SigningKey;
159
+ keyId?: string;
160
+ algorithm?: SigningAlgorithm;
161
+ now?: Date;
162
+ expiresInSeconds?: number;
163
+ approval?: Approval;
164
+ parentPassId?: string | null;
165
+ ledgerHash?: string | null;
166
+ traceId?: string;
167
+ };
168
+
169
+ export type IssueActionPassResult = {
170
+ token: string;
171
+ claims: ActionPassClaims;
172
+ };
173
+
174
+ export type AuthorizeInput = {
175
+ action: unknown;
176
+ issuer: string;
177
+ tenant: string;
178
+ signingKey: SigningKey;
179
+ keyId?: string;
180
+ algorithm?: SigningAlgorithm;
181
+ now?: Date;
182
+ policy?: PolicyOptions;
183
+ previousLedgerHash?: string | null;
184
+ };
185
+
186
+ export type AuthorizeResult = {
187
+ action: NormalizedAction;
188
+ decision: PolicyDecision;
189
+ payloadHash: string;
190
+ actionPass: IssueActionPassResult | null;
191
+ ledger: LedgerRecord;
192
+ };
193
+
194
+ export type VerifyActionPassInput = {
195
+ token: string;
196
+ action: unknown;
197
+ verificationKey: VerificationKey;
198
+ issuer?: string;
199
+ audience?: string | string[];
200
+ algorithms?: SigningAlgorithm[];
201
+ clockTolerance?: string | number;
202
+ };
203
+
204
+ export type VerifyActionPassResult =
205
+ | {
206
+ valid: true;
207
+ claims: ActionPassClaims;
208
+ payloadHash: string;
209
+ }
210
+ | {
211
+ valid: false;
212
+ reason: string;
213
+ };
214
+
215
+ export const demoAction: NormalizedAction = NormalizedActionSchema.parse({
216
+ actor: {
217
+ agentId: "agent:codex-prod",
218
+ humanOwner: "user:asrar@company.com",
219
+ runtime: "codex-cli",
220
+ tenant: "org:company",
221
+ },
222
+ intent: {
223
+ taskId: "AXT-418",
224
+ declaredGoal: "Open a PR that fixes the auth redirect bug",
225
+ },
226
+ capability: {
227
+ tool: "github.pull_requests.create",
228
+ resource: "repo:company/web-app",
229
+ payload: {
230
+ baseBranch: "main",
231
+ filesChanged: ["src/app/login/page.tsx", "src/lib/auth/session.ts"],
232
+ touchesProduction: false,
233
+ },
234
+ },
235
+ });
236
+
237
+ export function parseNormalizedAction(action: unknown): NormalizedAction {
238
+ return NormalizedActionSchema.parse(action);
239
+ }
240
+
241
+ export function evaluateAction(
242
+ actionInput: unknown,
243
+ options: PolicyOptions = {}
244
+ ): PolicyDecision {
245
+ const action = parseNormalizedAction(actionInput);
246
+ const payload = action.capability.payload;
247
+ const filesChanged = getStringArray(payload.filesChanged);
248
+ const requiredBaseBranch = options.requiredBaseBranch ?? "main";
249
+ const maxFilesChanged = options.maxFilesChanged ?? 12;
250
+ const blockedPathPrefixes = options.blockedPathPrefixes ?? [
251
+ "infra/prod/",
252
+ "billing/",
253
+ ".env",
254
+ ];
255
+ const policy = {
256
+ nativeRule: "github_pr_coding_agent_v0",
257
+ version: options.policyVersion ?? "2026-05-30",
258
+ cedarCompatible: true,
259
+ opaCompatible: true,
260
+ };
261
+ const constraints = {
262
+ expiresInSeconds: DEFAULT_EXPIRES_IN_SECONDS,
263
+ maxFilesChanged,
264
+ blockedPaths: ["infra/prod/**", "billing/**", ".env*"],
265
+ };
266
+ const stepUpReasons: string[] = [];
267
+
268
+ if (action.capability.tool !== (options.allowedTool ?? "github.pull_requests.create")) {
269
+ return {
270
+ decision: "deny",
271
+ reasons: [
272
+ `unsupported_tool: ${action.capability.tool} is not enabled by this policy`,
273
+ ],
274
+ policy,
275
+ constraints,
276
+ };
277
+ }
278
+
279
+ if (payload.baseBranch !== requiredBaseBranch) {
280
+ stepUpReasons.push(`base_branch_mismatch: expected ${requiredBaseBranch}`);
281
+ }
282
+
283
+ if (filesChanged.length > maxFilesChanged) {
284
+ stepUpReasons.push("file_count_exceeds_policy_limit");
285
+ }
286
+
287
+ if (
288
+ filesChanged.some((path) =>
289
+ blockedPathPrefixes.some((prefix) => path.startsWith(prefix))
290
+ )
291
+ ) {
292
+ stepUpReasons.push("payload_touches_protected_path");
293
+ }
294
+
295
+ if (payload.touchesProduction === true) {
296
+ stepUpReasons.push("payload_declares_production_impact");
297
+ }
298
+
299
+ if (stepUpReasons.length > 0) {
300
+ return {
301
+ decision: "step_up",
302
+ reasons: stepUpReasons,
303
+ policy,
304
+ constraints,
305
+ };
306
+ }
307
+
308
+ return {
309
+ decision: "allow",
310
+ reasons: ["payload_is_inside_current_actionpass_constraints"],
311
+ policy,
312
+ constraints,
313
+ };
314
+ }
315
+
316
+ export async function authorize(input: AuthorizeInput): Promise<AuthorizeResult> {
317
+ const action = parseNormalizedAction(input.action);
318
+ const decision = evaluateAction(action, input.policy);
319
+ const payloadHash = hashPayload(action.capability.payload);
320
+ const actionPass =
321
+ decision.decision === "allow"
322
+ ? await issueActionPass({
323
+ action,
324
+ decision,
325
+ issuer: input.issuer,
326
+ tenant: input.tenant,
327
+ signingKey: input.signingKey,
328
+ keyId: input.keyId,
329
+ algorithm: input.algorithm,
330
+ now: input.now,
331
+ })
332
+ : null;
333
+ const ledger = recordDecision({
334
+ action,
335
+ decision,
336
+ actionPassId: actionPass?.claims.jti ?? null,
337
+ previousLedgerHash: input.previousLedgerHash ?? null,
338
+ occurredAt: input.now,
339
+ });
340
+
341
+ return {
342
+ action,
343
+ decision,
344
+ payloadHash,
345
+ actionPass,
346
+ ledger,
347
+ };
348
+ }
349
+
350
+ export async function issueActionPass(
351
+ input: IssueActionPassInput
352
+ ): Promise<IssueActionPassResult> {
353
+ const action = parseNormalizedAction(input.action);
354
+ const decision = PolicyDecisionSchema.parse(input.decision);
355
+
356
+ if (decision.decision !== "allow") {
357
+ throw new Error("ActionPass can only be issued for an allow decision.");
358
+ }
359
+
360
+ const now = input.now ?? new Date();
361
+ const issuedAt = toNumericDate(now);
362
+ const expiresInSeconds =
363
+ input.expiresInSeconds ?? decision.constraints.expiresInSeconds;
364
+ const expiresAt = issuedAt + expiresInSeconds;
365
+ const passId = `ap_${randomUUID()}`;
366
+ const claims: ActionPassClaims = ActionPassClaimsSchema.parse({
367
+ apv: ACTIONPASS_VERSION,
368
+ iss: input.issuer,
369
+ sub: action.actor.agentId,
370
+ aud: action.capability.resource,
371
+ exp: expiresAt,
372
+ nbf: issuedAt,
373
+ iat: issuedAt,
374
+ jti: passId,
375
+ tenant: input.tenant,
376
+ humanOwner: action.actor.humanOwner,
377
+ agentRuntime: action.actor.runtime,
378
+ intent: action.intent,
379
+ capability: {
380
+ tool: action.capability.tool,
381
+ resource: action.capability.resource,
382
+ constraints: action.capability.constraints,
383
+ },
384
+ decision: decision.decision,
385
+ reasons: decision.reasons,
386
+ policy: decision.policy,
387
+ payloadHash: hashPayload(action.capability.payload),
388
+ approval: input.approval,
389
+ audit: {
390
+ traceId: input.traceId ?? `trace_${randomUUID()}`,
391
+ parentPassId: input.parentPassId ?? null,
392
+ ledgerHash: input.ledgerHash ?? null,
393
+ },
394
+ });
395
+
396
+ const token = await new SignJWT(claims)
397
+ .setProtectedHeader({
398
+ alg: input.algorithm ?? DEFAULT_SIGNING_ALGORITHM,
399
+ typ: "axtary-actionpass+jwt",
400
+ kid: input.keyId,
401
+ })
402
+ .sign(input.signingKey);
403
+
404
+ return { token, claims };
405
+ }
406
+
407
+ export async function verifyActionPass(
408
+ input: VerifyActionPassInput
409
+ ): Promise<VerifyActionPassResult> {
410
+ try {
411
+ const action = parseNormalizedAction(input.action);
412
+ const { payload } = await jwtVerify(input.token, input.verificationKey, {
413
+ issuer: input.issuer,
414
+ audience: input.audience ?? action.capability.resource,
415
+ algorithms: input.algorithms ?? [DEFAULT_SIGNING_ALGORITHM],
416
+ clockTolerance: input.clockTolerance,
417
+ });
418
+ const claims = ActionPassClaimsSchema.parse(payload);
419
+ const payloadHash = hashPayload(action.capability.payload);
420
+ const bindingError = validatePassBinding(claims, action, payloadHash);
421
+
422
+ if (bindingError) {
423
+ return {
424
+ valid: false,
425
+ reason: bindingError,
426
+ };
427
+ }
428
+
429
+ return {
430
+ valid: true,
431
+ claims,
432
+ payloadHash,
433
+ };
434
+ } catch (error) {
435
+ return {
436
+ valid: false,
437
+ reason: error instanceof Error ? error.message : "verification_failed",
438
+ };
439
+ }
440
+ }
441
+
442
+ export function recordDecision(input: {
443
+ action: unknown;
444
+ decision: PolicyDecision;
445
+ actionPassId?: string | null;
446
+ previousLedgerHash?: string | null;
447
+ occurredAt?: Date;
448
+ }): LedgerRecord {
449
+ const action = parseNormalizedAction(input.action);
450
+ const decision = PolicyDecisionSchema.parse(input.decision);
451
+ const payloadHash = hashPayload(action.capability.payload);
452
+ const actionHash = hashJson(action);
453
+ const recordWithoutHash = {
454
+ schemaVersion: LEDGER_SCHEMA_VERSION,
455
+ id: `ledger_${randomUUID()}`,
456
+ occurredAt: (input.occurredAt ?? new Date()).toISOString(),
457
+ actionHash,
458
+ payloadHash,
459
+ decision: decision.decision,
460
+ reasons: decision.reasons,
461
+ policy: decision.policy,
462
+ actionPassId: input.actionPassId ?? null,
463
+ previousLedgerHash: input.previousLedgerHash ?? null,
464
+ };
465
+
466
+ return LedgerRecordSchema.parse({
467
+ ...recordWithoutHash,
468
+ ledgerHash: hashJson(recordWithoutHash),
469
+ });
470
+ }
471
+
472
+ export function hashPayload(payload: unknown): string {
473
+ return hashJson(JsonValueSchema.parse(payload));
474
+ }
475
+
476
+ export function hashJson(value: unknown): string {
477
+ return `sha256:${createHash("sha256")
478
+ .update(stableStringify(value))
479
+ .digest("hex")}`;
480
+ }
481
+
482
+ export function stableStringify(value: unknown): string {
483
+ return JSON.stringify(canonicalize(value));
484
+ }
485
+
486
+ function validatePassBinding(
487
+ claims: ActionPassClaims,
488
+ action: NormalizedAction,
489
+ payloadHash: string
490
+ ): string | null {
491
+ if (claims.sub !== action.actor.agentId) {
492
+ return "agent_id_mismatch";
493
+ }
494
+ if (claims.humanOwner !== action.actor.humanOwner) {
495
+ return "human_owner_mismatch";
496
+ }
497
+ if (claims.agentRuntime !== action.actor.runtime) {
498
+ return "agent_runtime_mismatch";
499
+ }
500
+ if (claims.intent.taskId !== action.intent.taskId) {
501
+ return "task_id_mismatch";
502
+ }
503
+ if (claims.capability.tool !== action.capability.tool) {
504
+ return "tool_mismatch";
505
+ }
506
+ if (claims.capability.resource !== action.capability.resource) {
507
+ return "resource_mismatch";
508
+ }
509
+ if (claims.payloadHash !== payloadHash) {
510
+ return "payload_hash_mismatch";
511
+ }
512
+
513
+ return null;
514
+ }
515
+
516
+ function getStringArray(value: JsonValue | undefined): string[] {
517
+ if (!Array.isArray(value)) {
518
+ return [];
519
+ }
520
+
521
+ return value.filter((item): item is string => typeof item === "string");
522
+ }
523
+
524
+ function toNumericDate(date: Date): number {
525
+ return Math.floor(date.getTime() / 1000);
526
+ }
527
+
528
+ function canonicalize(value: unknown): unknown {
529
+ if (Array.isArray(value)) {
530
+ return value.map((item) => canonicalize(item));
531
+ }
532
+
533
+ if (isPlainObject(value)) {
534
+ return Object.fromEntries(
535
+ Object.entries(value)
536
+ .filter(([, entryValue]) => entryValue !== undefined)
537
+ .sort(([left], [right]) => left.localeCompare(right))
538
+ .map(([key, entryValue]) => [key, canonicalize(entryValue)])
539
+ );
540
+ }
541
+
542
+ return value;
543
+ }
544
+
545
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
546
+ return (
547
+ typeof value === "object" &&
548
+ value !== null &&
549
+ !Array.isArray(value) &&
550
+ Object.getPrototypeOf(value) === Object.prototype
551
+ );
552
+ }