@decionis/mcp 0.1.2 → 0.1.4
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/.claude-plugin/plugin.json +15 -0
- package/README.md +67 -5
- package/commands/init.md +23 -0
- package/dist/AgentHookCli.js +2 -2
- package/dist/ManagedAgentHookCli.js +18440 -8210
- package/dist/agent/AgentIndex.d.ts +37 -2
- package/dist/agent/AgentIndex.js +2 -2
- package/dist/{chunk-2C62WFX2.js → chunk-MJVKY7PZ.js} +28 -6
- package/dist/chunk-MJVKY7PZ.js.map +1 -0
- package/dist/{chunk-KY5AVRL6.js → chunk-YVRIRS6H.js} +13505 -2577
- package/dist/chunk-YVRIRS6H.js.map +1 -0
- package/dist/index.js +2233 -8
- package/dist/index.js.map +1 -1
- package/hooks/hooks.json +17 -0
- package/package.json +8 -3
- package/templates/ClaudeSettings.json +1 -1
- package/templates/DECIONIS_POLICY.starter.md +108 -0
- package/templates/managed/ClaudeManagedSettings.Windows.json +1 -1
- package/templates/managed/ClaudeManagedSettings.json +1 -1
- package/dist/chunk-2C62WFX2.js.map +0 -1
- package/dist/chunk-KY5AVRL6.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import {
|
|
3
3
|
LocalDecionisGateway,
|
|
4
4
|
PolicyFileRepository,
|
|
5
|
+
external_exports,
|
|
5
6
|
normalizeVerdict,
|
|
6
7
|
sha256Hex
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YVRIRS6H.js";
|
|
8
9
|
|
|
9
10
|
// src/Server.ts
|
|
10
11
|
import { createInterface } from "readline";
|
|
11
12
|
var SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
12
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
13
|
+
var MCP_SERVER_VERSION = "0.1.4";
|
|
13
14
|
var DEFAULT_PROTOCOL_VERSION = "2025-11-25";
|
|
14
15
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
16
|
function rpcResult(id, result) {
|
|
@@ -53,7 +54,10 @@ var McpMessageHandler = class {
|
|
|
53
54
|
title,
|
|
54
55
|
description,
|
|
55
56
|
inputSchema,
|
|
56
|
-
annotations
|
|
57
|
+
annotations: {
|
|
58
|
+
title,
|
|
59
|
+
...annotations
|
|
60
|
+
}
|
|
57
61
|
})
|
|
58
62
|
)
|
|
59
63
|
});
|
|
@@ -164,10 +168,55 @@ rule matches, the platform holds the decision for review (outcome REVIEW).
|
|
|
164
168
|
action you intend to take (e.g. {"action": "production-deploy", "agent_generated": true})
|
|
165
169
|
and abide by the verdict: allow \u2192 proceed; block \u2192 do not proceed; review/escalate \u2192
|
|
166
170
|
stop and ask a human.`;
|
|
171
|
+
var DEMO_POLICY = `# Decionis demo policy
|
|
172
|
+
|
|
173
|
+
Three rules and a default: refunds under the desk limit run, refunds over it
|
|
174
|
+
wait for a named approver, deploys during a change freeze never run, and
|
|
175
|
+
anything the policy has no opinion on is held for review.
|
|
176
|
+
|
|
177
|
+
\`\`\`decionis
|
|
178
|
+
{ "version": 1, "rules": [
|
|
179
|
+
{ "name": "Block production deploys during a change freeze",
|
|
180
|
+
"priority": 100, "domain": "*",
|
|
181
|
+
"all": [{ "field": "context.change_freeze", "op": "eq", "value": true }],
|
|
182
|
+
"action": "block" },
|
|
183
|
+
{ "name": "Escalate refunds over the desk limit",
|
|
184
|
+
"priority": 90, "domain": "*",
|
|
185
|
+
"all": [{ "field": "context.amount", "op": "gt", "value": 1000 }],
|
|
186
|
+
"action": "escalate" },
|
|
187
|
+
{ "name": "Allow refunds within the desk limit",
|
|
188
|
+
"priority": 80, "domain": "*",
|
|
189
|
+
"all": [{ "field": "context.amount", "op": "lte", "value": 1000 }],
|
|
190
|
+
"action": "allow" } ] }
|
|
191
|
+
\`\`\`
|
|
192
|
+
`;
|
|
193
|
+
var DEMO_INTENTS = [
|
|
194
|
+
{
|
|
195
|
+
label: "Refund $180 \u2014 within the desk limit",
|
|
196
|
+
payload: { action: "issue-refund", amount: 180, agent_generated: true },
|
|
197
|
+
shows: "Within policy: execution continues, and the rule that allowed it is named."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
label: "Refund $7,500 \u2014 over the desk limit",
|
|
201
|
+
payload: { action: "issue-refund", amount: 7500, agent_generated: true },
|
|
202
|
+
shows: "Held short of execution and routed to a named approver \u2014 not refused."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
label: "Production deploy during a change freeze",
|
|
206
|
+
payload: { action: "production-deploy", change_freeze: true, agent_generated: true },
|
|
207
|
+
shows: "Refused outright: nobody is asked to approve it."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: "Export customer data \u2014 no rule covers it",
|
|
211
|
+
payload: { action: "export-customer-data", destination: "personal-email" },
|
|
212
|
+
shows: "No rule matched, so the decision queues for review \u2014 unmatched intents fail safe."
|
|
213
|
+
}
|
|
214
|
+
];
|
|
167
215
|
var DecionisTools = class {
|
|
168
|
-
constructor(gateway2, policyFiles = new PolicyFileRepository()) {
|
|
216
|
+
constructor(gateway2, policyFiles = new PolicyFileRepository(), additionalTools = []) {
|
|
169
217
|
this.gateway = gateway2;
|
|
170
218
|
this.policyFiles = policyFiles;
|
|
219
|
+
this.additionalTools = additionalTools;
|
|
171
220
|
}
|
|
172
221
|
build() {
|
|
173
222
|
const gateway2 = this.gateway;
|
|
@@ -175,7 +224,7 @@ var DecionisTools = class {
|
|
|
175
224
|
{
|
|
176
225
|
name: "decionis_read_policy",
|
|
177
226
|
title: "Read Repository Policy",
|
|
178
|
-
description: "Read and compile the repo's DECIONIS_POLICY.md
|
|
227
|
+
description: "Read and compile the repo's DECIONIS_POLICY.md execution policy: path, sha256, and the enforceable rules (name, verdict, priority, domain) or compile errors. Use this to learn what this repository allows, blocks, restrains, or escalates.",
|
|
179
228
|
inputSchema: {
|
|
180
229
|
type: "object",
|
|
181
230
|
properties: {
|
|
@@ -297,6 +346,67 @@ var DecionisTools = class {
|
|
|
297
346
|
};
|
|
298
347
|
}
|
|
299
348
|
},
|
|
349
|
+
{
|
|
350
|
+
name: "decionis_demo",
|
|
351
|
+
title: "Run the One-Minute Demo",
|
|
352
|
+
description: "See Decionis work in under a minute: runs four canned intents (refund within limit, refund over limit, deploy during a change freeze, an action no rule covers) through the real protocol evaluator with a built-in sample policy. Entirely local \u2014 no account, no API key, no network, no policy file needed, nothing recorded. Call this on first contact with Decionis; use decionis_evaluate to gate this repo's own actions afterwards.",
|
|
353
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
354
|
+
annotations: {
|
|
355
|
+
readOnlyHint: true,
|
|
356
|
+
destructiveHint: false,
|
|
357
|
+
idempotentHint: true,
|
|
358
|
+
openWorldHint: false
|
|
359
|
+
},
|
|
360
|
+
handler: async () => {
|
|
361
|
+
const published = await gateway2.publishPolicy(DEMO_POLICY, { workflowKey: null });
|
|
362
|
+
if (!published.ok) {
|
|
363
|
+
return {
|
|
364
|
+
content: text({ error: "demo policy failed to compile", details: published.errors }),
|
|
365
|
+
isError: true
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const decisions = [];
|
|
369
|
+
for (const intent of DEMO_INTENTS) {
|
|
370
|
+
const evaluated = await gateway2.evaluate({
|
|
371
|
+
payload: intent.payload,
|
|
372
|
+
workflowKey: null,
|
|
373
|
+
mode: "shadow"
|
|
374
|
+
});
|
|
375
|
+
if (evaluated.status !== 200) {
|
|
376
|
+
return {
|
|
377
|
+
content: text({
|
|
378
|
+
error: `protocol evaluation failed (HTTP ${evaluated.status})`,
|
|
379
|
+
intent: intent.label,
|
|
380
|
+
body: evaluated.body
|
|
381
|
+
}),
|
|
382
|
+
isError: true
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const body = evaluated.body;
|
|
386
|
+
const policyEvaluation = isPlainObject2(body.policy_evaluation) ? body.policy_evaluation : {};
|
|
387
|
+
decisions.push({
|
|
388
|
+
intent: intent.label,
|
|
389
|
+
outcome: body.outcome,
|
|
390
|
+
verdict: normalizeVerdict(body.outcome),
|
|
391
|
+
selected_rule: policyEvaluation.selected_rule_name ?? null,
|
|
392
|
+
what_this_shows: intent.shows
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
content: text({
|
|
397
|
+
headline: "Four intents, four outcomes \u2014 each decided by policy before execution, through the same evaluator that runs in production.",
|
|
398
|
+
decisions,
|
|
399
|
+
vocabulary: "Wire outcomes are APPROVE / ESCALATE / REJECT / REVIEW \u2014 the wire spellings of the three decision classes (allow / escalate / block). ESCALATE routes to a named approver; REVIEW queues without one.",
|
|
400
|
+
note: "Local sandbox \u2014 the sample policy above, nothing recorded, no signed dossier minted. In production every decision leaves an Ed25519-signed Decision Dossier anyone can verify offline against the published JWKS.",
|
|
401
|
+
next_steps: [
|
|
402
|
+
"Gate this repo's own actions: decionis_read_policy, then decionis_evaluate before anything risky.",
|
|
403
|
+
"Watch real traffic read-only first: https://decionis.com/shadow-mode",
|
|
404
|
+
"First governed decision with signed proof: https://decionis.com/docs/quickstart"
|
|
405
|
+
]
|
|
406
|
+
})
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
},
|
|
300
410
|
{
|
|
301
411
|
name: "decionis_verdict_help",
|
|
302
412
|
title: "Explain Decionis Verdicts",
|
|
@@ -309,19 +419,2134 @@ var DecionisTools = class {
|
|
|
309
419
|
openWorldHint: false
|
|
310
420
|
},
|
|
311
421
|
handler: async () => ({ content: text(VERDICT_HELP) })
|
|
422
|
+
},
|
|
423
|
+
...this.additionalTools
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
function createDecionisTools(gateway2, additionalTools = []) {
|
|
428
|
+
return new DecionisTools(gateway2, new PolicyFileRepository(), additionalTools).build();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/kubernetes/KubernetesActionPlanner.ts
|
|
432
|
+
import { randomUUID } from "crypto";
|
|
433
|
+
|
|
434
|
+
// ../../packages/sdk-node/dist/index.js
|
|
435
|
+
import { createHash } from "crypto";
|
|
436
|
+
import { createHash as createHash2 } from "crypto";
|
|
437
|
+
var DEFAULT_SKEW_MS = 6 * 60 * 60 * 1e3;
|
|
438
|
+
var authoritySelectorSchema = external_exports.discriminatedUnion("kind", [
|
|
439
|
+
external_exports.object({
|
|
440
|
+
kind: external_exports.literal("ROLE"),
|
|
441
|
+
role_id: external_exports.string().regex(/^[A-Z][A-Z0-9_]{1,63}$/)
|
|
442
|
+
}).strict(),
|
|
443
|
+
external_exports.object({
|
|
444
|
+
kind: external_exports.literal("PRINCIPAL"),
|
|
445
|
+
principal_id: external_exports.string().min(1).max(200)
|
|
446
|
+
}).strict()
|
|
447
|
+
]);
|
|
448
|
+
var authorityVerificationRequirementsSchema = external_exports.object({
|
|
449
|
+
level: external_exports.enum(["STANDARD", "HIGH_CONFIDENCE"]),
|
|
450
|
+
methods: external_exports.array(external_exports.enum(["WEBAUTHN", "MOBILE_DEVICE", "ACTIVE_LIVENESS"])).min(1).max(3),
|
|
451
|
+
hardware_pki_required: external_exports.boolean(),
|
|
452
|
+
disallow_virtual_cameras: external_exports.boolean(),
|
|
453
|
+
mobile_device_policy: external_exports.enum(["WEB_OR_JIT_DEVICE", "MANAGED_DEVICE_REQUIRED"]).optional(),
|
|
454
|
+
trusted_ip_regions: external_exports.array(external_exports.string().trim().min(1).max(64)).min(1).max(100).optional()
|
|
455
|
+
}).strict().superRefine((requirements, context) => {
|
|
456
|
+
if (new Set(requirements.methods).size !== requirements.methods.length) {
|
|
457
|
+
context.addIssue({
|
|
458
|
+
code: external_exports.ZodIssueCode.custom,
|
|
459
|
+
path: ["methods"],
|
|
460
|
+
message: "verification methods must be unique"
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (requirements.trusted_ip_regions !== void 0 && new Set(requirements.trusted_ip_regions).size !== requirements.trusted_ip_regions.length) {
|
|
464
|
+
context.addIssue({
|
|
465
|
+
code: external_exports.ZodIssueCode.custom,
|
|
466
|
+
path: ["trusted_ip_regions"],
|
|
467
|
+
message: "trusted IP regions must be unique"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
var authorityResolutionPolicySchema = external_exports.object({
|
|
472
|
+
quorum: external_exports.literal(1),
|
|
473
|
+
delegation: external_exports.enum(["DISALLOWED", "REQUIREMENT_REFRESH"]),
|
|
474
|
+
fallback_role_ids: external_exports.array(external_exports.string().regex(/^[A-Z][A-Z0-9_]{1,63}$/)).max(10).default([])
|
|
475
|
+
}).strict().superRefine((policy, context) => {
|
|
476
|
+
if (new Set(policy.fallback_role_ids).size !== policy.fallback_role_ids.length) {
|
|
477
|
+
context.addIssue({
|
|
478
|
+
code: external_exports.ZodIssueCode.custom,
|
|
479
|
+
path: ["fallback_role_ids"],
|
|
480
|
+
message: "fallback role ids must be unique"
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
var originatorSchema = external_exports.object({
|
|
485
|
+
organization_id: external_exports.string().min(1).max(200),
|
|
486
|
+
organization_name: external_exports.string().trim().min(1).max(160),
|
|
487
|
+
actor_id: external_exports.string().min(1).max(200).optional(),
|
|
488
|
+
display_name: external_exports.string().trim().min(1).max(160).optional(),
|
|
489
|
+
role: external_exports.string().trim().min(1).max(120).optional(),
|
|
490
|
+
provenance: external_exports.enum(["AUTHENTICATED_TENANT", "VERIFIED_CONNECTOR", "CALLER_DECLARED"])
|
|
491
|
+
}).strict();
|
|
492
|
+
var displayFieldSchema = external_exports.object({
|
|
493
|
+
key: external_exports.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
|
|
494
|
+
label: external_exports.string().trim().min(1).max(120),
|
|
495
|
+
value: external_exports.string().trim().min(1).max(500)
|
|
496
|
+
}).strict();
|
|
497
|
+
var presentationSchema = external_exports.object({
|
|
498
|
+
locale: external_exports.string().min(2).max(35).regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/),
|
|
499
|
+
title: external_exports.string().trim().min(1).max(160),
|
|
500
|
+
description: external_exports.string().trim().min(1).max(1e3),
|
|
501
|
+
display_fields: external_exports.array(displayFieldSchema).max(20)
|
|
502
|
+
}).strict().superRefine((presentation, context) => {
|
|
503
|
+
const keys = presentation.display_fields.map((field) => field.key);
|
|
504
|
+
if (new Set(keys).size !== keys.length) {
|
|
505
|
+
context.addIssue({
|
|
506
|
+
code: external_exports.ZodIssueCode.custom,
|
|
507
|
+
path: ["display_fields"],
|
|
508
|
+
message: "display field keys must be unique"
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
var decionisAuthorityRequirementSchema = external_exports.object({
|
|
513
|
+
requirement_version: external_exports.literal("decionis.authority_requirement/1"),
|
|
514
|
+
requirement_id: external_exports.string().min(8).max(200),
|
|
515
|
+
source_decision_id: external_exports.string().min(1).max(200),
|
|
516
|
+
source_dossier_id: external_exports.string().min(1).max(200),
|
|
517
|
+
policy_version: external_exports.string().min(1).max(120),
|
|
518
|
+
policy_reason_codes: external_exports.array(external_exports.string().regex(/^[A-Z][A-Z0-9_]{1,119}$/)).min(1).max(20),
|
|
519
|
+
action_digest: external_exports.string().regex(/^[0-9a-f]{64}$/),
|
|
520
|
+
selector: authoritySelectorSchema,
|
|
521
|
+
verification_requirements: authorityVerificationRequirementsSchema,
|
|
522
|
+
resolution: authorityResolutionPolicySchema,
|
|
523
|
+
originator: originatorSchema,
|
|
524
|
+
presentation: presentationSchema,
|
|
525
|
+
nonce: external_exports.string().regex(/^[A-Za-z0-9_-]{43,128}$/),
|
|
526
|
+
issued_at: external_exports.string().datetime(),
|
|
527
|
+
expires_at: external_exports.string().datetime()
|
|
528
|
+
}).strict().superRefine((requirement, context) => {
|
|
529
|
+
if (Date.parse(requirement.expires_at) <= Date.parse(requirement.issued_at)) {
|
|
530
|
+
context.addIssue({
|
|
531
|
+
code: external_exports.ZodIssueCode.custom,
|
|
532
|
+
path: ["expires_at"],
|
|
533
|
+
message: "expires_at must be later than issued_at"
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (requirement.selector.kind === "ROLE" && requirement.resolution.fallback_role_ids.includes(requirement.selector.role_id)) {
|
|
537
|
+
context.addIssue({
|
|
538
|
+
code: external_exports.ZodIssueCode.custom,
|
|
539
|
+
path: ["resolution", "fallback_role_ids"],
|
|
540
|
+
message: "fallback roles must not repeat the primary role"
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
var decionisEvaluateModeSchema = external_exports.enum(["SHADOW", "PARALLEL", "ENFORCEMENT"]);
|
|
545
|
+
var decionisEvaluateOutcomeSchema = external_exports.enum(["APPROVE", "ESCALATE", "REJECT", "REVIEW"]);
|
|
546
|
+
var decionisEvaluateDecisionRequestSchema = external_exports.object({
|
|
547
|
+
org_id: external_exports.string().uuid(),
|
|
548
|
+
decision_type: external_exports.string().min(1).max(120),
|
|
549
|
+
amount: external_exports.number().optional(),
|
|
550
|
+
risk_score: external_exports.number().min(0).max(1).optional(),
|
|
551
|
+
channel: external_exports.string().min(1).max(120).optional(),
|
|
552
|
+
policy_version: external_exports.string().min(1).max(120).optional(),
|
|
553
|
+
objective_profile: external_exports.string().min(1).max(120).optional(),
|
|
554
|
+
mode: decionisEvaluateModeSchema.optional(),
|
|
555
|
+
decision_band: external_exports.string().min(1).max(120).optional(),
|
|
556
|
+
transaction_type: external_exports.string().min(1).max(120).optional(),
|
|
557
|
+
workflow_key: external_exports.string().min(1).max(120).optional(),
|
|
558
|
+
vertical_pack: external_exports.string().min(1).max(120).optional(),
|
|
559
|
+
context: external_exports.record(external_exports.unknown()).optional(),
|
|
560
|
+
idempotency_key: external_exports.string().min(1).max(180).optional()
|
|
561
|
+
});
|
|
562
|
+
var governanceMetricsSchema = external_exports.object({
|
|
563
|
+
override_drift_rate: external_exports.number(),
|
|
564
|
+
governance_tension_index: external_exports.number(),
|
|
565
|
+
boundary_volatility_score: external_exports.number(),
|
|
566
|
+
decision_volume_under_governance: external_exports.number()
|
|
567
|
+
});
|
|
568
|
+
var policyGuardSchema = external_exports.object({
|
|
569
|
+
status: external_exports.enum(["ready", "configured", "missing"]),
|
|
570
|
+
requires_manual_review: external_exports.boolean(),
|
|
571
|
+
total_bundle_count: external_exports.number(),
|
|
572
|
+
active_bundle_count: external_exports.number(),
|
|
573
|
+
active_bundle_id: external_exports.string().uuid().nullable(),
|
|
574
|
+
active_bundle_version: external_exports.string().nullable(),
|
|
575
|
+
active_effective_from: external_exports.string().nullable(),
|
|
576
|
+
active_effective_to: external_exports.string().nullable(),
|
|
577
|
+
latest_bundle_id: external_exports.string().uuid().nullable(),
|
|
578
|
+
latest_bundle_version: external_exports.string().nullable(),
|
|
579
|
+
latest_effective_from: external_exports.string().nullable(),
|
|
580
|
+
latest_effective_to: external_exports.string().nullable(),
|
|
581
|
+
reason: external_exports.enum([
|
|
582
|
+
"missing_org_policy_bundle",
|
|
583
|
+
"no_active_org_policy_bundle",
|
|
584
|
+
"requested_policy_bundle_not_found"
|
|
585
|
+
]).nullable(),
|
|
586
|
+
evaluated_at: external_exports.string()
|
|
587
|
+
}).optional();
|
|
588
|
+
var policyEvaluationSchema = external_exports.object({
|
|
589
|
+
resolution: external_exports.enum([
|
|
590
|
+
"policy_guard_blocked",
|
|
591
|
+
"matched_rule",
|
|
592
|
+
"no_matching_rule",
|
|
593
|
+
"missing_required_signals"
|
|
594
|
+
]),
|
|
595
|
+
bundle_id: external_exports.string().uuid().nullable(),
|
|
596
|
+
bundle_version: external_exports.string().nullable(),
|
|
597
|
+
evaluated_at: external_exports.string(),
|
|
598
|
+
decision_domain: external_exports.string(),
|
|
599
|
+
evaluated_rule_count: external_exports.number(),
|
|
600
|
+
matched_rule_count: external_exports.number(),
|
|
601
|
+
matched_rule_ids: external_exports.array(external_exports.string()),
|
|
602
|
+
selected_rule_id: external_exports.string().nullable(),
|
|
603
|
+
selected_rule_name: external_exports.string().nullable(),
|
|
604
|
+
selected_rule_action: external_exports.enum(["REQUIRE_REVIEW", "AUTO_APPROVE", "AUTO_REJECT", "ESCALATE", "REQUEST_INFO"]).nullable(),
|
|
605
|
+
selected_rule_priority: external_exports.number().nullable(),
|
|
606
|
+
selected_rule_rationale: external_exports.string().nullable(),
|
|
607
|
+
selected_rule_severity: external_exports.enum(["routine", "elevated", "urgent"]).nullable(),
|
|
608
|
+
selected_rule_assignee_role: external_exports.string().nullable(),
|
|
609
|
+
selected_rule_sla_minutes: external_exports.number().nullable(),
|
|
610
|
+
selected_rule_authority: external_exports.unknown().optional(),
|
|
611
|
+
missing_required_signals: external_exports.array(external_exports.string()).optional()
|
|
612
|
+
}).optional();
|
|
613
|
+
var policySnapshotSchema = external_exports.object({
|
|
614
|
+
policy_version: external_exports.string(),
|
|
615
|
+
policy_bundle_id: external_exports.string().uuid().nullable(),
|
|
616
|
+
rules_sha256: external_exports.string().nullable(),
|
|
617
|
+
parameters_sha256: external_exports.string().nullable(),
|
|
618
|
+
effective_from: external_exports.string().nullable(),
|
|
619
|
+
effective_to: external_exports.string().nullable(),
|
|
620
|
+
evaluated_at: external_exports.string(),
|
|
621
|
+
source: external_exports.enum(["org_policy_bundle", "policy_guard", "request_default"])
|
|
622
|
+
}).optional();
|
|
623
|
+
var decionisEvaluateDecisionResponseSchema = external_exports.object({
|
|
624
|
+
outcome: decionisEvaluateOutcomeSchema,
|
|
625
|
+
confidence: external_exports.number(),
|
|
626
|
+
policy_version: external_exports.string(),
|
|
627
|
+
objective_profile: external_exports.string(),
|
|
628
|
+
dossier_id: external_exports.string().uuid(),
|
|
629
|
+
dossier_sha256: external_exports.string().nullable().optional(),
|
|
630
|
+
evaluation_id: external_exports.string().uuid(),
|
|
631
|
+
decision_resolution_id: external_exports.string().uuid().nullable().optional(),
|
|
632
|
+
ledger_entry_id: external_exports.string().uuid().nullable().optional(),
|
|
633
|
+
ledger_entry_hash: external_exports.string().nullable().optional(),
|
|
634
|
+
mode: decionisEvaluateModeSchema,
|
|
635
|
+
reason_codes: external_exports.array(external_exports.string().min(1)).optional(),
|
|
636
|
+
fallback_to_legacy: external_exports.boolean(),
|
|
637
|
+
fallback_reason: external_exports.string().nullable().optional(),
|
|
638
|
+
reconciliation_required: external_exports.boolean().optional(),
|
|
639
|
+
reconciliation_status: external_exports.enum(["NOT_REQUIRED", "PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]).optional(),
|
|
640
|
+
decision_domain: external_exports.string().optional(),
|
|
641
|
+
governance_metrics: governanceMetricsSchema,
|
|
642
|
+
policy_guard: policyGuardSchema,
|
|
643
|
+
policy_evaluation: policyEvaluationSchema,
|
|
644
|
+
policy_snapshot: policySnapshotSchema.nullable().optional(),
|
|
645
|
+
authority_requirement: decionisAuthorityRequirementSchema.optional(),
|
|
646
|
+
idempotent_replay: external_exports.boolean()
|
|
647
|
+
});
|
|
648
|
+
var authorityDecisionSchema = external_exports.object({
|
|
649
|
+
decision_id: external_exports.string().min(1),
|
|
650
|
+
chain_id: external_exports.string().nullable(),
|
|
651
|
+
status: external_exports.enum(["ALLOW", "BLOCK", "ESCALATE", "REVIEW_REQUIRED", "ERROR"]),
|
|
652
|
+
should_execute: external_exports.boolean(),
|
|
653
|
+
reason_codes: external_exports.array(external_exports.string()),
|
|
654
|
+
action_hash: external_exports.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
655
|
+
policy_version: external_exports.string().nullable().optional().default(null),
|
|
656
|
+
mode: external_exports.enum(["SHADOW", "PARALLEL", "ENFORCEMENT"]).nullable().optional().default(null),
|
|
657
|
+
execution_token: external_exports.string().min(1).nullable(),
|
|
658
|
+
execution_token_expires_at: external_exports.string().datetime().nullable(),
|
|
659
|
+
dossier_id: external_exports.string().nullable(),
|
|
660
|
+
dossier_sha256: external_exports.string().nullable().optional().default(null),
|
|
661
|
+
dossier_url: external_exports.string().nullable(),
|
|
662
|
+
approval_request_id: external_exports.string().nullable().optional().default(null),
|
|
663
|
+
ledger_entry_id: external_exports.string().nullable()
|
|
664
|
+
}).passthrough();
|
|
665
|
+
var DecionisExecutionClientError = class extends Error {
|
|
666
|
+
status;
|
|
667
|
+
body;
|
|
668
|
+
constructor(message, options) {
|
|
669
|
+
super(message);
|
|
670
|
+
this.name = "DecionisExecutionClientError";
|
|
671
|
+
this.status = options.status;
|
|
672
|
+
this.body = options.body;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
async function postJson(options, route, body) {
|
|
676
|
+
const baseUrl = options.authorityBaseUrl.replace(/\/+$/, "");
|
|
677
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
678
|
+
const controller = new AbortController();
|
|
679
|
+
const timeout = setTimeout(
|
|
680
|
+
() => controller.abort(),
|
|
681
|
+
Math.min(Math.max(options.timeoutMs ?? 4e3, 1), 15e3)
|
|
682
|
+
);
|
|
683
|
+
try {
|
|
684
|
+
const response = await fetchImpl(`${baseUrl}${route}`, {
|
|
685
|
+
method: "POST",
|
|
686
|
+
headers: {
|
|
687
|
+
"content-type": "application/json",
|
|
688
|
+
...options.defaultHeaders ?? {}
|
|
689
|
+
},
|
|
690
|
+
body: JSON.stringify(body),
|
|
691
|
+
signal: controller.signal
|
|
692
|
+
});
|
|
693
|
+
const responseText = await response.text();
|
|
694
|
+
const responseBody = Buffer.byteLength(responseText, "utf8") <= 100 * 1024 ? (() => {
|
|
695
|
+
try {
|
|
696
|
+
return JSON.parse(responseText);
|
|
697
|
+
} catch {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
})() : null;
|
|
701
|
+
if (!response.ok) {
|
|
702
|
+
throw new DecionisExecutionClientError(
|
|
703
|
+
`Decionis execution authority request failed with status ${response.status}`,
|
|
704
|
+
{
|
|
705
|
+
status: response.status,
|
|
706
|
+
body: responseBody
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
return responseBody;
|
|
711
|
+
} finally {
|
|
712
|
+
clearTimeout(timeout);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
var DecionisExecutionClient = class {
|
|
716
|
+
options;
|
|
717
|
+
constructor(options) {
|
|
718
|
+
assertAuthorityUrl(options);
|
|
719
|
+
this.options = options;
|
|
720
|
+
}
|
|
721
|
+
enforceAndBind = async (request) => {
|
|
722
|
+
const response = await postJson(
|
|
723
|
+
{
|
|
724
|
+
...this.options,
|
|
725
|
+
defaultHeaders: {
|
|
726
|
+
"idempotency-key": request.intent_id,
|
|
727
|
+
...this.options.defaultHeaders ?? {}
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
"/v1/authority/enforce-and-bind",
|
|
731
|
+
request
|
|
732
|
+
);
|
|
733
|
+
return authorityDecisionSchema.parse(response);
|
|
734
|
+
};
|
|
735
|
+
verifyToken = (body) => {
|
|
736
|
+
return postJson(
|
|
737
|
+
this.options,
|
|
738
|
+
"/v1/execution/verify-token",
|
|
739
|
+
body
|
|
740
|
+
);
|
|
741
|
+
};
|
|
742
|
+
consumeToken = (body) => {
|
|
743
|
+
return postJson(
|
|
744
|
+
this.options,
|
|
745
|
+
"/v1/execution/consume-token",
|
|
746
|
+
body
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
claimToken = (body) => {
|
|
750
|
+
return postJson(
|
|
751
|
+
this.options,
|
|
752
|
+
"/v1/execution/claim-token",
|
|
753
|
+
body
|
|
754
|
+
);
|
|
755
|
+
};
|
|
756
|
+
/**
|
|
757
|
+
* Grant-required, fail-closed enforcement — the default path. Delegates to
|
|
758
|
+
* {@link enforceAndExecuteStrict}: the authority must authorize AND issue a
|
|
759
|
+
* single-use grant, and that grant must redeem, BEFORE `execute` runs. Policy
|
|
760
|
+
* block, missing grant, failed redemption, and an unreachable authority all
|
|
761
|
+
* resolve to `executed: false` (`decision` is `null` on authority errors)
|
|
762
|
+
* instead of running the action. Call the strict method directly when the
|
|
763
|
+
* caller needs the grant redemption or the precise non-execution reason;
|
|
764
|
+
* this wrapper keeps the smaller executed/decision/result shape.
|
|
765
|
+
*/
|
|
766
|
+
enforceAndExecute = async (input) => {
|
|
767
|
+
const strict = await this.enforceAndExecuteStrict({
|
|
768
|
+
request: input.request,
|
|
769
|
+
execute: ({ decision, executionToken }) => input.execute({ decision, executionToken })
|
|
770
|
+
});
|
|
771
|
+
if (!strict.executed) {
|
|
772
|
+
return {
|
|
773
|
+
executed: false,
|
|
774
|
+
decision: strict.decision,
|
|
775
|
+
result: null
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
executed: true,
|
|
780
|
+
decision: strict.decision,
|
|
781
|
+
result: strict.result
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
/**
|
|
785
|
+
* Redeem a single-use execution grant via `/v1/execution/consume-token`, binding
|
|
786
|
+
* it to the exact action. Never throws — a transport/HTTP failure returns an
|
|
787
|
+
* invalid redemption so the strict path fails closed rather than executing on an
|
|
788
|
+
* unverified grant.
|
|
789
|
+
*/
|
|
790
|
+
redeemGrant = async (executionToken, request, expectedDecision, commitCorrelationId = request.intent_id) => {
|
|
791
|
+
const baseUrl = this.options.authorityBaseUrl.replace(/\/+$/, "");
|
|
792
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
793
|
+
const controller = new AbortController();
|
|
794
|
+
const timeout = setTimeout(
|
|
795
|
+
() => controller.abort(),
|
|
796
|
+
Math.min(Math.max(this.options.timeoutMs ?? 4e3, 1), 15e3)
|
|
797
|
+
);
|
|
798
|
+
try {
|
|
799
|
+
const response = await fetchImpl(`${baseUrl}/v1/execution/claim-token`, {
|
|
800
|
+
method: "POST",
|
|
801
|
+
headers: { "content-type": "application/json", ...this.options.defaultHeaders ?? {} },
|
|
802
|
+
body: JSON.stringify({
|
|
803
|
+
execution_token: executionToken,
|
|
804
|
+
intent_hash: request.intent_hash,
|
|
805
|
+
intent: authorityIntentBindingOf(request),
|
|
806
|
+
...request.evidence ? { evidence: request.evidence } : {},
|
|
807
|
+
consumed_by: request.actor.id,
|
|
808
|
+
commit_correlation_id: commitCorrelationId
|
|
809
|
+
}),
|
|
810
|
+
signal: controller.signal
|
|
811
|
+
});
|
|
812
|
+
const responseText = await response.text();
|
|
813
|
+
const body = (() => {
|
|
814
|
+
if (Buffer.byteLength(responseText, "utf8") > 100 * 1024) return null;
|
|
815
|
+
try {
|
|
816
|
+
return JSON.parse(responseText);
|
|
817
|
+
} catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
})();
|
|
821
|
+
if (!response.ok || !body || body.valid !== true) {
|
|
822
|
+
const reason_codes = Array.isArray(body?.reason_codes) ? body?.reason_codes : ["DECIONIS_GRANT_REJECTED"];
|
|
823
|
+
return { valid: false, reason_codes, claims: body?.claims ?? null };
|
|
824
|
+
}
|
|
825
|
+
const claims = body.claims;
|
|
826
|
+
const binding = claims && typeof claims.binding === "object" && !Array.isArray(claims.binding) ? claims.binding : null;
|
|
827
|
+
if (!claims || typeof claims.jti !== "string" || claims.org_id !== request.tenant_id || claims.sub !== request.actor.id || claims.action !== request.action.type || binding?.intent_hash !== request.intent_hash || expectedDecision !== void 0 && (claims.decision_id !== expectedDecision.decision_id || claims.dossier_id !== expectedDecision.dossier_id)) {
|
|
828
|
+
return {
|
|
829
|
+
valid: false,
|
|
830
|
+
reason_codes: ["DECIONIS_GRANT_BINDING_MISMATCH"],
|
|
831
|
+
claims: null
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
valid: true,
|
|
836
|
+
reason_codes: Array.isArray(body.reason_codes) ? body.reason_codes : [],
|
|
837
|
+
claims,
|
|
838
|
+
claim_token: typeof body.claim_token === "string" ? body.claim_token : null,
|
|
839
|
+
claim_lease_expires_at: typeof body.claim_lease_expires_at === "string" ? body.claim_lease_expires_at : null,
|
|
840
|
+
...body.evidence && typeof body.evidence === "object" ? { evidence: body.evidence } : {}
|
|
841
|
+
};
|
|
842
|
+
} catch {
|
|
843
|
+
return { valid: false, reason_codes: ["DECIONIS_GRANT_REDEEM_ERROR"], claims: null };
|
|
844
|
+
} finally {
|
|
845
|
+
clearTimeout(timeout);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
finalizeGrant = async (input) => {
|
|
849
|
+
try {
|
|
850
|
+
const response = await postJson(
|
|
851
|
+
this.options,
|
|
852
|
+
"/v1/execution/finalize-token",
|
|
853
|
+
{
|
|
854
|
+
execution_token: input.executionToken,
|
|
855
|
+
claim_token: input.claimToken,
|
|
856
|
+
outcome: input.outcome,
|
|
857
|
+
commit_correlation_id: input.commitCorrelationId,
|
|
858
|
+
...input.downstreamEvidence ? { downstream_evidence: input.downstreamEvidence } : {}
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
return response.finalized === true;
|
|
862
|
+
} catch {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* Grant-required (strict) enforcement: "no valid grant, no execution." The default
|
|
868
|
+
* {@link enforceAndExecute} delegates to this path. This makes the single-use
|
|
869
|
+
* execution grant a hard precondition: the authority must authorize AND issue a
|
|
870
|
+
* grant, and that grant must redeem successfully, BEFORE `execute` runs. Because
|
|
871
|
+
* the grant is redeemed up front, a downstream failure requires a fresh decision
|
|
872
|
+
* rather than replaying an ALLOW — the fail-closed posture for consequential
|
|
873
|
+
* actions. Every non-executing path returns `executed: false` with a reason.
|
|
874
|
+
*/
|
|
875
|
+
enforceAndExecuteStrict = async (input) => {
|
|
876
|
+
let decision;
|
|
877
|
+
try {
|
|
878
|
+
decision = await this.enforceAndBind(input.request);
|
|
879
|
+
} catch {
|
|
880
|
+
return {
|
|
881
|
+
executed: false,
|
|
882
|
+
decision: null,
|
|
883
|
+
grant: null,
|
|
884
|
+
reason: "AUTHORITY_ERROR",
|
|
885
|
+
result: null
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (!decision.should_execute) {
|
|
889
|
+
return { executed: false, decision, grant: null, reason: "BLOCKED_BY_POLICY", result: null };
|
|
890
|
+
}
|
|
891
|
+
if (!decision.execution_token) {
|
|
892
|
+
return { executed: false, decision, grant: null, reason: "NO_EXECUTION_GRANT", result: null };
|
|
893
|
+
}
|
|
894
|
+
const commitCorrelationId = input.request.intent_id;
|
|
895
|
+
const grant = await this.redeemGrant(
|
|
896
|
+
decision.execution_token,
|
|
897
|
+
input.request,
|
|
898
|
+
decision,
|
|
899
|
+
commitCorrelationId
|
|
900
|
+
);
|
|
901
|
+
if (!grant.valid || !grant.claim_token) {
|
|
902
|
+
return { executed: false, decision, grant, reason: "GRANT_NOT_REDEEMABLE", result: null };
|
|
903
|
+
}
|
|
904
|
+
let result;
|
|
905
|
+
try {
|
|
906
|
+
result = await input.execute({
|
|
907
|
+
decision,
|
|
908
|
+
executionToken: decision.execution_token,
|
|
909
|
+
grant,
|
|
910
|
+
claimToken: grant.claim_token,
|
|
911
|
+
commitCorrelationId
|
|
912
|
+
});
|
|
913
|
+
} catch (error) {
|
|
914
|
+
await this.finalizeGrant({
|
|
915
|
+
executionToken: decision.execution_token,
|
|
916
|
+
claimToken: grant.claim_token,
|
|
917
|
+
outcome: input.classifyFailure?.(error) ?? "INDETERMINATE",
|
|
918
|
+
commitCorrelationId,
|
|
919
|
+
downstreamEvidence: { error_type: error instanceof Error ? error.name : "unknown" }
|
|
920
|
+
});
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
const finalized = await this.finalizeGrant({
|
|
924
|
+
executionToken: decision.execution_token,
|
|
925
|
+
claimToken: grant.claim_token,
|
|
926
|
+
outcome: "COMMITTED",
|
|
927
|
+
commitCorrelationId
|
|
928
|
+
});
|
|
929
|
+
if (!finalized) grant.reason_codes = [...grant.reason_codes, "COMMIT_FINALIZATION_PENDING"];
|
|
930
|
+
return { executed: true, decision, grant, result };
|
|
931
|
+
};
|
|
932
|
+
};
|
|
933
|
+
function createDecionisExecutionClient(options) {
|
|
934
|
+
return new DecionisExecutionClient(options);
|
|
935
|
+
}
|
|
936
|
+
function authorityIntentBindingOf(request) {
|
|
937
|
+
return {
|
|
938
|
+
protocol_version: request.protocol_version,
|
|
939
|
+
tenant_id: request.tenant_id,
|
|
940
|
+
intent_id: request.intent_id,
|
|
941
|
+
captured_at: request.captured_at,
|
|
942
|
+
expires_at: request.expires_at,
|
|
943
|
+
actor: request.actor,
|
|
944
|
+
action: request.action,
|
|
945
|
+
context: request.context,
|
|
946
|
+
downstream_target: request.downstream_target
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function hashAuthorityIntentBinding(binding) {
|
|
950
|
+
const canonical = stableJsonStringify(binding);
|
|
951
|
+
if (Buffer.byteLength(canonical, "utf8") > 100 * 1024) {
|
|
952
|
+
throw new Error("DECIONIS_INTENT_TOO_LARGE");
|
|
953
|
+
}
|
|
954
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
955
|
+
}
|
|
956
|
+
function bindAuthorityIntent(binding, evidence, mode = "ENFORCEMENT") {
|
|
957
|
+
return {
|
|
958
|
+
...binding,
|
|
959
|
+
intent_hash: hashAuthorityIntentBinding(binding),
|
|
960
|
+
mode,
|
|
961
|
+
...evidence === void 0 ? {} : { evidence }
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function stableJsonStringify(value) {
|
|
965
|
+
let entries = 0;
|
|
966
|
+
const normalize = (current, depth) => {
|
|
967
|
+
if (depth > 20) throw new Error("DECIONIS_INTENT_TOO_DEEP");
|
|
968
|
+
if (current === null || typeof current === "string" || typeof current === "boolean") {
|
|
969
|
+
return current;
|
|
970
|
+
}
|
|
971
|
+
if (typeof current === "number") {
|
|
972
|
+
if (!Number.isFinite(current)) throw new Error("DECIONIS_INTENT_NUMBER_INVALID");
|
|
973
|
+
return current;
|
|
974
|
+
}
|
|
975
|
+
if (Array.isArray(current)) {
|
|
976
|
+
if (current.length > 1e3) throw new Error("DECIONIS_INTENT_ARRAY_TOO_LARGE");
|
|
977
|
+
entries += current.length;
|
|
978
|
+
if (entries > 5e3) throw new Error("DECIONIS_INTENT_TOO_MANY_ENTRIES");
|
|
979
|
+
return current.map((item) => normalize(item, depth + 1));
|
|
980
|
+
}
|
|
981
|
+
if (!current || typeof current !== "object") throw new Error("DECIONIS_INTENT_VALUE_INVALID");
|
|
982
|
+
const source = current;
|
|
983
|
+
const keys = Object.keys(source).sort();
|
|
984
|
+
entries += keys.length;
|
|
985
|
+
if (entries > 5e3) throw new Error("DECIONIS_INTENT_TOO_MANY_ENTRIES");
|
|
986
|
+
const normalized = {};
|
|
987
|
+
for (const key of keys) {
|
|
988
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
989
|
+
throw new Error("DECIONIS_INTENT_KEY_UNSAFE");
|
|
990
|
+
}
|
|
991
|
+
normalized[key] = normalize(source[key], depth + 1);
|
|
992
|
+
}
|
|
993
|
+
return normalized;
|
|
994
|
+
};
|
|
995
|
+
return JSON.stringify(normalize(value, 0));
|
|
996
|
+
}
|
|
997
|
+
function assertAuthorityUrl(options) {
|
|
998
|
+
const url = new URL(options.authorityBaseUrl);
|
|
999
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
1000
|
+
if (url.username || url.password) throw new Error("DECIONIS_URL_MUST_NOT_CONTAIN_CREDENTIALS");
|
|
1001
|
+
if (url.protocol !== "https:" && !(loopback && options.allowInsecureLoopback === true)) {
|
|
1002
|
+
throw new Error("DECIONIS_URL_MUST_USE_HTTPS");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function bindKubernetesAuthorityAction(input) {
|
|
1006
|
+
const group = input.apiGroup || "core";
|
|
1007
|
+
const actionType = `kubernetes.${input.operation.toLowerCase()}.${token(group)}.${token(input.resource)}`.slice(
|
|
1008
|
+
0,
|
|
1009
|
+
120
|
|
1010
|
+
);
|
|
1011
|
+
const parameters = {
|
|
1012
|
+
cluster_id: input.clusterId,
|
|
1013
|
+
operation: input.operation,
|
|
1014
|
+
api_group: input.apiGroup,
|
|
1015
|
+
api_version: input.apiVersion,
|
|
1016
|
+
kind: input.kind,
|
|
1017
|
+
resource: input.resource,
|
|
1018
|
+
subresource: input.subresource ?? null,
|
|
1019
|
+
namespace: input.namespace ?? null,
|
|
1020
|
+
name: input.name ?? null,
|
|
1021
|
+
old_object_digest: input.oldObjectDigest,
|
|
1022
|
+
proposed_object_digest: input.proposedObjectDigest,
|
|
1023
|
+
material_diff: input.materialDiff,
|
|
1024
|
+
material_facts: input.materialFacts,
|
|
1025
|
+
policy_version: input.policyVersion,
|
|
1026
|
+
policy_rule: input.policyRule,
|
|
1027
|
+
dry_run: input.dryRun ?? false
|
|
1028
|
+
};
|
|
1029
|
+
return bindAuthorityIntent(
|
|
1030
|
+
{
|
|
1031
|
+
protocol_version: "agent-safe.intent/1",
|
|
1032
|
+
tenant_id: input.organizationId,
|
|
1033
|
+
intent_id: input.intentId,
|
|
1034
|
+
captured_at: input.capturedAt,
|
|
1035
|
+
expires_at: input.expiresAt,
|
|
1036
|
+
actor: input.actor,
|
|
1037
|
+
action: {
|
|
1038
|
+
type: actionType,
|
|
1039
|
+
resource: [
|
|
1040
|
+
input.clusterId,
|
|
1041
|
+
group,
|
|
1042
|
+
input.apiVersion,
|
|
1043
|
+
input.resource,
|
|
1044
|
+
input.subresource ?? "-",
|
|
1045
|
+
input.namespace ?? "-",
|
|
1046
|
+
input.name ?? "-"
|
|
1047
|
+
].map((part) => encodeURIComponent(part)).join(":").slice(0, 500),
|
|
1048
|
+
parameters
|
|
1049
|
+
},
|
|
1050
|
+
context: {
|
|
1051
|
+
source: input.requestSource ?? "kubernetes",
|
|
1052
|
+
kubernetes: {
|
|
1053
|
+
...parameters,
|
|
1054
|
+
request_source: input.requestSource ?? null,
|
|
1055
|
+
...input.additionalKubernetesContext ?? {}
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
downstream_target: {
|
|
1059
|
+
system: `kubernetes:${input.clusterId}`,
|
|
1060
|
+
operation: input.operation,
|
|
1061
|
+
endpoint: [
|
|
1062
|
+
group,
|
|
1063
|
+
input.apiVersion,
|
|
1064
|
+
input.resource,
|
|
1065
|
+
input.namespace ?? "-",
|
|
1066
|
+
input.name ?? "-"
|
|
1067
|
+
].map((part) => encodeURIComponent(part)).join("/").slice(0, 500)
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
void 0,
|
|
1071
|
+
input.mode
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
function hashKubernetesObject(input) {
|
|
1075
|
+
if (!input) return null;
|
|
1076
|
+
return `sha256:${createHash2("sha256").update(stableJson(input), "utf8").digest("hex")}`;
|
|
1077
|
+
}
|
|
1078
|
+
function summarizeKubernetesObjectDiff(oldObject, proposedObject, secret) {
|
|
1079
|
+
const paths = [];
|
|
1080
|
+
collectChangedPaths(oldObject, proposedObject, "", paths, secret);
|
|
1081
|
+
const unique = [...new Set(paths)].sort();
|
|
1082
|
+
return {
|
|
1083
|
+
changed_path_count: unique.length,
|
|
1084
|
+
changed_paths: unique.slice(0, 64),
|
|
1085
|
+
truncated: unique.length > 64
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function deriveKubernetesMaterialFacts(input) {
|
|
1089
|
+
const object = input.object ?? input.oldObject;
|
|
1090
|
+
const facts = {};
|
|
1091
|
+
const replicas = finiteNumber(record(input.object?.spec).replicas);
|
|
1092
|
+
if (replicas !== null && replicas >= 0) facts.proposed_replicas = replicas;
|
|
1093
|
+
const workloadSpec = podSpec(object, input.kind);
|
|
1094
|
+
if (workloadSpec) {
|
|
1095
|
+
const containers = [
|
|
1096
|
+
...records(workloadSpec.containers),
|
|
1097
|
+
...records(workloadSpec.initContainers),
|
|
1098
|
+
...records(workloadSpec.ephemeralContainers)
|
|
1099
|
+
];
|
|
1100
|
+
const serviceAccount = string(workloadSpec.serviceAccountName) ?? string(workloadSpec.serviceAccount);
|
|
1101
|
+
facts.workload_security = {
|
|
1102
|
+
privileged_containers: containers.filter(
|
|
1103
|
+
(container) => record(container.securityContext).privileged === true
|
|
1104
|
+
).length,
|
|
1105
|
+
allow_privilege_escalation_containers: containers.filter(
|
|
1106
|
+
(container) => record(container.securityContext).allowPrivilegeEscalation === true
|
|
1107
|
+
).length,
|
|
1108
|
+
host_network: workloadSpec.hostNetwork === true,
|
|
1109
|
+
host_pid: workloadSpec.hostPID === true,
|
|
1110
|
+
host_ipc: workloadSpec.hostIPC === true,
|
|
1111
|
+
host_path_volumes: records(workloadSpec.volumes).filter((volume) => isRecord(volume.hostPath)).length,
|
|
1112
|
+
host_port_bindings: containers.reduce(
|
|
1113
|
+
(count, container) => count + records(container.ports).filter((port) => finiteNumber(port.hostPort) !== null).length,
|
|
1114
|
+
0
|
|
1115
|
+
),
|
|
1116
|
+
gpu_count: containers.reduce((count, container) => count + gpuCount(container), 0),
|
|
1117
|
+
...serviceAccount ? {
|
|
1118
|
+
workload_service_account_sha256: createHash2("sha256").update(serviceAccount, "utf8").digest("hex")
|
|
1119
|
+
} : {}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (input.apiGroup === "rbac.authorization.k8s.io" && ["roles", "clusterroles", "rolebindings", "clusterrolebindings"].includes(input.resource)) {
|
|
1123
|
+
const roleRef = record(object?.roleRef);
|
|
1124
|
+
const rules = records(object?.rules);
|
|
1125
|
+
facts.rbac = {
|
|
1126
|
+
binds_cluster_admin: string(roleRef.name) === "cluster-admin" && string(roleRef.kind) === "ClusterRole",
|
|
1127
|
+
wildcard_rules: rules.filter((rule) => {
|
|
1128
|
+
const verbs = strings(rule.verbs);
|
|
1129
|
+
const resources = strings(rule.resources);
|
|
1130
|
+
const apiGroups = strings(rule.apiGroups);
|
|
1131
|
+
return verbs.includes("*") || resources.includes("*") || apiGroups.includes("*");
|
|
1132
|
+
}).length,
|
|
1133
|
+
privilege_escalation_rules: rules.filter(
|
|
1134
|
+
(rule) => strings(rule.verbs).some((verb) => ["bind", "escalate", "impersonate"].includes(verb))
|
|
1135
|
+
).length
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
if (input.kind === "Secret") facts.sensitive_object = true;
|
|
1139
|
+
return facts;
|
|
1140
|
+
}
|
|
1141
|
+
function collectChangedPaths(before, after, path, paths, secret) {
|
|
1142
|
+
if (stableJson(before) === stableJson(after)) return;
|
|
1143
|
+
if (secret && (path === "/data" || path === "/stringData")) {
|
|
1144
|
+
paths.push(`${path}/<redacted>`);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (isRecord(before) && isRecord(after)) {
|
|
1148
|
+
for (const key of [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort()) {
|
|
1149
|
+
collectChangedPaths(before[key], after[key], `${path}/${pointer(key)}`, paths, secret);
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
paths.push(path || "/");
|
|
1154
|
+
}
|
|
1155
|
+
function stableJson(value) {
|
|
1156
|
+
if (value === void 0) return "undefined";
|
|
1157
|
+
const normalize = (current) => {
|
|
1158
|
+
if (Array.isArray(current)) return current.map(normalize);
|
|
1159
|
+
if (!isRecord(current)) return current;
|
|
1160
|
+
return Object.fromEntries(
|
|
1161
|
+
Object.keys(current).sort().map((key) => [key, normalize(current[key])])
|
|
1162
|
+
);
|
|
1163
|
+
};
|
|
1164
|
+
return JSON.stringify(normalize(value));
|
|
1165
|
+
}
|
|
1166
|
+
function podSpec(object, kind) {
|
|
1167
|
+
const spec = record(object?.spec);
|
|
1168
|
+
if (kind === "Pod") return spec;
|
|
1169
|
+
if (kind === "CronJob") {
|
|
1170
|
+
const jobSpec = record(record(spec.jobTemplate).spec);
|
|
1171
|
+
const cronPodSpec = record(record(jobSpec.template).spec);
|
|
1172
|
+
return Object.keys(cronPodSpec).length > 0 ? cronPodSpec : null;
|
|
1173
|
+
}
|
|
1174
|
+
const workloadPodSpec = record(record(spec.template).spec);
|
|
1175
|
+
return Object.keys(workloadPodSpec).length > 0 ? workloadPodSpec : null;
|
|
1176
|
+
}
|
|
1177
|
+
function gpuCount(container) {
|
|
1178
|
+
const resources = record(container.resources);
|
|
1179
|
+
const requests = record(resources.requests);
|
|
1180
|
+
const limits = record(resources.limits);
|
|
1181
|
+
let total = 0;
|
|
1182
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(requests), ...Object.keys(limits)])) {
|
|
1183
|
+
if (!/(^|[./-])gpu$/i.test(key)) continue;
|
|
1184
|
+
total += Math.max(quantity(requests[key]), quantity(limits[key]));
|
|
1185
|
+
}
|
|
1186
|
+
return total;
|
|
1187
|
+
}
|
|
1188
|
+
function quantity(input) {
|
|
1189
|
+
if (typeof input === "number" && Number.isFinite(input) && input >= 0) return input;
|
|
1190
|
+
if (typeof input !== "string" || !/^\d+(?:\.\d+)?$/.test(input)) return 0;
|
|
1191
|
+
const parsed = Number(input);
|
|
1192
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
1193
|
+
}
|
|
1194
|
+
function finiteNumber(input) {
|
|
1195
|
+
return typeof input === "number" && Number.isFinite(input) ? input : null;
|
|
1196
|
+
}
|
|
1197
|
+
function string(input) {
|
|
1198
|
+
return typeof input === "string" && input.length > 0 ? input : null;
|
|
1199
|
+
}
|
|
1200
|
+
function strings(input) {
|
|
1201
|
+
return Array.isArray(input) ? input.filter((value) => typeof value === "string") : [];
|
|
1202
|
+
}
|
|
1203
|
+
function records(input) {
|
|
1204
|
+
return Array.isArray(input) ? input.filter(isRecord) : [];
|
|
1205
|
+
}
|
|
1206
|
+
function record(input) {
|
|
1207
|
+
return isRecord(input) ? input : {};
|
|
1208
|
+
}
|
|
1209
|
+
function isRecord(input) {
|
|
1210
|
+
return !!input && typeof input === "object" && !Array.isArray(input);
|
|
1211
|
+
}
|
|
1212
|
+
function pointer(input) {
|
|
1213
|
+
return input.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1214
|
+
}
|
|
1215
|
+
function token(input) {
|
|
1216
|
+
return input.toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^[^a-z]+/, "x");
|
|
1217
|
+
}
|
|
1218
|
+
var personalDeviceSchema = external_exports.object({
|
|
1219
|
+
device_id: external_exports.string(),
|
|
1220
|
+
org_id: external_exports.string(),
|
|
1221
|
+
label: external_exports.string(),
|
|
1222
|
+
scopes: external_exports.array(external_exports.string()),
|
|
1223
|
+
registered_at: external_exports.string()
|
|
1224
|
+
});
|
|
1225
|
+
var personalProtocolSchema = external_exports.object({
|
|
1226
|
+
outcome: external_exports.string(),
|
|
1227
|
+
dossier_id: external_exports.string(),
|
|
1228
|
+
evaluation_id: external_exports.string().optional(),
|
|
1229
|
+
policy_version: external_exports.string().optional(),
|
|
1230
|
+
verification_url: external_exports.string().optional()
|
|
1231
|
+
}).passthrough();
|
|
1232
|
+
var personalCapsuleSchema = external_exports.object({
|
|
1233
|
+
capsule_id: external_exports.string(),
|
|
1234
|
+
org_id: external_exports.string(),
|
|
1235
|
+
title: external_exports.string(),
|
|
1236
|
+
state_hash: external_exports.string(),
|
|
1237
|
+
payload_ref: external_exports.string(),
|
|
1238
|
+
dossier_id: external_exports.string(),
|
|
1239
|
+
verification_url: external_exports.string(),
|
|
1240
|
+
evidence_hashes: external_exports.array(external_exports.string()),
|
|
1241
|
+
captured_at: external_exports.string(),
|
|
1242
|
+
retention_days: external_exports.number(),
|
|
1243
|
+
deleted_at: external_exports.string().nullable()
|
|
1244
|
+
});
|
|
1245
|
+
var registerDeviceResponseSchema = external_exports.object({
|
|
1246
|
+
device: personalDeviceSchema,
|
|
1247
|
+
token: external_exports.string()
|
|
1248
|
+
});
|
|
1249
|
+
var captureCapsuleResponseSchema = external_exports.object({
|
|
1250
|
+
capsule: personalCapsuleSchema,
|
|
1251
|
+
protocol: personalProtocolSchema
|
|
1252
|
+
});
|
|
1253
|
+
var resumeCapsuleResponseSchema = external_exports.object({
|
|
1254
|
+
verdict: external_exports.literal("RESTORE"),
|
|
1255
|
+
execution: external_exports.object({
|
|
1256
|
+
type: external_exports.string(),
|
|
1257
|
+
parameters: external_exports.record(external_exports.unknown())
|
|
1258
|
+
}).optional(),
|
|
1259
|
+
protocol: personalProtocolSchema
|
|
1260
|
+
});
|
|
1261
|
+
var recallResponseSchema = external_exports.object({
|
|
1262
|
+
verdict: external_exports.enum(["NO_ACTION", "PIN_RESOURCE", "LOAD_SIDEBAR", "OPEN_RESOURCE"]),
|
|
1263
|
+
dispatch: external_exports.object({
|
|
1264
|
+
type: external_exports.enum(["PIN_RESOURCE", "LOAD_SIDEBAR", "OPEN_RESOURCE"]),
|
|
1265
|
+
parameters: external_exports.object({
|
|
1266
|
+
resource_url: external_exports.string(),
|
|
1267
|
+
label: external_exports.string().nullable(),
|
|
1268
|
+
rule_id: external_exports.string()
|
|
1269
|
+
})
|
|
1270
|
+
}).nullable(),
|
|
1271
|
+
protocol: personalProtocolSchema.optional()
|
|
1272
|
+
});
|
|
1273
|
+
var personalConfigResponseSchema = external_exports.object({
|
|
1274
|
+
service: external_exports.literal("personal"),
|
|
1275
|
+
org_id: external_exports.string(),
|
|
1276
|
+
config_version: external_exports.string().nullable(),
|
|
1277
|
+
device: external_exports.object({
|
|
1278
|
+
device_id: external_exports.string().nullable(),
|
|
1279
|
+
auth_type: external_exports.string().nullable(),
|
|
1280
|
+
scopes: external_exports.array(external_exports.string())
|
|
1281
|
+
}),
|
|
1282
|
+
policy: external_exports.object({
|
|
1283
|
+
bundle_id: external_exports.string(),
|
|
1284
|
+
version: external_exports.string(),
|
|
1285
|
+
rules_sha256: external_exports.string(),
|
|
1286
|
+
parameters_sha256: external_exports.string(),
|
|
1287
|
+
rule_count: external_exports.number(),
|
|
1288
|
+
source: external_exports.string(),
|
|
1289
|
+
stored_at: external_exports.string()
|
|
1290
|
+
}).nullable(),
|
|
1291
|
+
realms: external_exports.record(external_exports.unknown()),
|
|
1292
|
+
recall_rules: external_exports.array(external_exports.record(external_exports.unknown()))
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// src/kubernetes/KubernetesActionPlanner.ts
|
|
1296
|
+
var KubernetesActionPlanner = class {
|
|
1297
|
+
constructor(config, runner, now = () => /* @__PURE__ */ new Date(), createId = randomUUID) {
|
|
1298
|
+
this.config = config;
|
|
1299
|
+
this.runner = runner;
|
|
1300
|
+
this.now = now;
|
|
1301
|
+
this.createId = createId;
|
|
1302
|
+
}
|
|
1303
|
+
discoveryCache = /* @__PURE__ */ new Map();
|
|
1304
|
+
async planManifest(manifestPath, namespace, source) {
|
|
1305
|
+
if (!manifestPath.trim() || manifestPath.length > 2e3 || /[\r\n\0]/.test(manifestPath)) {
|
|
1306
|
+
throw new Error("KUBERNETES_MANIFEST_PATH_INVALID");
|
|
1307
|
+
}
|
|
1308
|
+
const namespaceArgs = namespace ? ["--namespace", namespace] : [];
|
|
1309
|
+
const proposed = this.objects(
|
|
1310
|
+
await this.jsonCommand([
|
|
1311
|
+
"create",
|
|
1312
|
+
"--dry-run=client",
|
|
1313
|
+
"--output=json",
|
|
1314
|
+
"--filename",
|
|
1315
|
+
manifestPath,
|
|
1316
|
+
...namespaceArgs
|
|
1317
|
+
])
|
|
1318
|
+
);
|
|
1319
|
+
if (proposed.length === 0 || proposed.length > 100) {
|
|
1320
|
+
throw new Error("KUBERNETES_MANIFEST_OBJECT_COUNT_INVALID");
|
|
1321
|
+
}
|
|
1322
|
+
const current = this.objects(
|
|
1323
|
+
await this.jsonCommand([
|
|
1324
|
+
"get",
|
|
1325
|
+
"--ignore-not-found",
|
|
1326
|
+
"--output=json",
|
|
1327
|
+
"--filename",
|
|
1328
|
+
manifestPath,
|
|
1329
|
+
...namespaceArgs
|
|
1330
|
+
])
|
|
1331
|
+
);
|
|
1332
|
+
return Promise.all(
|
|
1333
|
+
proposed.map(async (object) => {
|
|
1334
|
+
const identity = this.identity(object, namespace);
|
|
1335
|
+
const oldObject = current.find((candidate) => this.sameIdentity(candidate, identity)) ?? null;
|
|
1336
|
+
const resource = await this.resourceName(
|
|
1337
|
+
identity.apiGroup,
|
|
1338
|
+
identity.apiVersion,
|
|
1339
|
+
identity.kind
|
|
1340
|
+
);
|
|
1341
|
+
return this.build({
|
|
1342
|
+
object,
|
|
1343
|
+
oldObject,
|
|
1344
|
+
resource,
|
|
1345
|
+
subresource: null,
|
|
1346
|
+
source,
|
|
1347
|
+
...identity
|
|
1348
|
+
});
|
|
1349
|
+
})
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
async planScale(input) {
|
|
1353
|
+
if (!Number.isInteger(input.replicas) || input.replicas < 0 || input.replicas > 1e6) {
|
|
1354
|
+
throw new Error("KUBERNETES_REPLICA_COUNT_INVALID");
|
|
1355
|
+
}
|
|
1356
|
+
const endpoint = `/apis/apps/v1/namespaces/${encodeURIComponent(input.namespace)}/deployments/${encodeURIComponent(input.name)}/scale`;
|
|
1357
|
+
const oldObject = this.record(await this.jsonCommand(["get", "--raw", endpoint]));
|
|
1358
|
+
const object = {
|
|
1359
|
+
...oldObject,
|
|
1360
|
+
spec: { ...this.record(oldObject.spec), replicas: input.replicas }
|
|
1361
|
+
};
|
|
1362
|
+
return this.build({
|
|
1363
|
+
object,
|
|
1364
|
+
oldObject,
|
|
1365
|
+
apiGroup: "apps",
|
|
1366
|
+
apiVersion: "v1",
|
|
1367
|
+
kind: "Scale",
|
|
1368
|
+
resource: "deployments",
|
|
1369
|
+
subresource: "scale",
|
|
1370
|
+
namespace: input.namespace,
|
|
1371
|
+
name: input.name,
|
|
1372
|
+
source: "mcp:k8s_scale",
|
|
1373
|
+
executionEndpoint: endpoint
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
build(input) {
|
|
1377
|
+
const capturedAt = this.now();
|
|
1378
|
+
const proposedObjectDigest = hashKubernetesObject(input.object);
|
|
1379
|
+
if (!proposedObjectDigest) throw new Error("KUBERNETES_OBJECT_DIGEST_MISSING");
|
|
1380
|
+
const oldObjectDigest = hashKubernetesObject(input.oldObject);
|
|
1381
|
+
const materialDiff = summarizeKubernetesObjectDiff(
|
|
1382
|
+
input.oldObject,
|
|
1383
|
+
input.object,
|
|
1384
|
+
input.kind === "Secret"
|
|
1385
|
+
);
|
|
1386
|
+
const materialFacts = deriveKubernetesMaterialFacts({
|
|
1387
|
+
object: input.object,
|
|
1388
|
+
oldObject: input.oldObject,
|
|
1389
|
+
kind: input.kind,
|
|
1390
|
+
apiGroup: input.apiGroup,
|
|
1391
|
+
resource: input.resource
|
|
1392
|
+
});
|
|
1393
|
+
const operation = input.oldObject ? "UPDATE" : "CREATE";
|
|
1394
|
+
const request = bindKubernetesAuthorityAction({
|
|
1395
|
+
organizationId: this.config.organizationId,
|
|
1396
|
+
clusterId: this.config.clusterId,
|
|
1397
|
+
policyVersion: this.config.policyVersion,
|
|
1398
|
+
policyRule: "mcp-client-gate",
|
|
1399
|
+
mode: "ENFORCEMENT",
|
|
1400
|
+
intentId: this.createId(),
|
|
1401
|
+
capturedAt: capturedAt.toISOString(),
|
|
1402
|
+
expiresAt: new Date(capturedAt.valueOf() + this.config.grantTtlSeconds * 1e3).toISOString(),
|
|
1403
|
+
actor: {
|
|
1404
|
+
id: this.config.actorId,
|
|
1405
|
+
type: "KUBERNETES_CLIENT",
|
|
1406
|
+
runtime: "mcp",
|
|
1407
|
+
trust_level: "MCP_CONFIGURED"
|
|
1408
|
+
},
|
|
1409
|
+
operation,
|
|
1410
|
+
apiGroup: input.apiGroup,
|
|
1411
|
+
apiVersion: input.apiVersion,
|
|
1412
|
+
kind: input.kind,
|
|
1413
|
+
resource: input.resource,
|
|
1414
|
+
subresource: input.subresource,
|
|
1415
|
+
namespace: input.namespace,
|
|
1416
|
+
name: input.name,
|
|
1417
|
+
oldObjectDigest,
|
|
1418
|
+
proposedObjectDigest,
|
|
1419
|
+
materialDiff,
|
|
1420
|
+
materialFacts,
|
|
1421
|
+
requestSource: input.source
|
|
1422
|
+
});
|
|
1423
|
+
return {
|
|
1424
|
+
request,
|
|
1425
|
+
operation,
|
|
1426
|
+
apiGroup: input.apiGroup,
|
|
1427
|
+
apiVersion: input.apiVersion,
|
|
1428
|
+
kind: input.kind,
|
|
1429
|
+
resource: input.resource,
|
|
1430
|
+
namespace: input.namespace,
|
|
1431
|
+
name: input.name,
|
|
1432
|
+
oldObjectDigest,
|
|
1433
|
+
proposedObjectDigest,
|
|
1434
|
+
materialDiff,
|
|
1435
|
+
materialFacts,
|
|
1436
|
+
executionObject: input.object,
|
|
1437
|
+
executionEndpoint: input.executionEndpoint ?? null
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
async resourceName(apiGroup, apiVersion, kind) {
|
|
1441
|
+
const key = `${apiGroup}/${apiVersion}`;
|
|
1442
|
+
let resources = this.discoveryCache.get(key);
|
|
1443
|
+
if (!resources) {
|
|
1444
|
+
const endpoint = apiGroup ? `/apis/${encodeURIComponent(apiGroup)}/${encodeURIComponent(apiVersion)}` : `/api/${encodeURIComponent(apiVersion)}`;
|
|
1445
|
+
const discovery = this.record(await this.jsonCommand(["get", "--raw", endpoint]));
|
|
1446
|
+
resources = Array.isArray(discovery.resources) ? discovery.resources.flatMap((candidate) => {
|
|
1447
|
+
const value = this.record(candidate);
|
|
1448
|
+
return typeof value.name === "string" && typeof value.kind === "string" && !value.name.includes("/") ? [{ name: value.name, kind: value.kind }] : [];
|
|
1449
|
+
}) : [];
|
|
1450
|
+
this.discoveryCache.set(key, resources);
|
|
1451
|
+
}
|
|
1452
|
+
const matched = resources.find((candidate) => candidate.kind === kind);
|
|
1453
|
+
if (!matched) throw new Error("KUBERNETES_RESOURCE_DISCOVERY_FAILED");
|
|
1454
|
+
return matched.name;
|
|
1455
|
+
}
|
|
1456
|
+
identity(object, fallbackNamespace) {
|
|
1457
|
+
const apiVersionValue = typeof object.apiVersion === "string" ? object.apiVersion : "";
|
|
1458
|
+
const kind = typeof object.kind === "string" ? object.kind : "";
|
|
1459
|
+
const metadata = this.record(object.metadata);
|
|
1460
|
+
const name = typeof metadata.name === "string" ? metadata.name : "";
|
|
1461
|
+
if (!apiVersionValue || !kind || !name) throw new Error("KUBERNETES_OBJECT_IDENTITY_INVALID");
|
|
1462
|
+
const separator = apiVersionValue.indexOf("/");
|
|
1463
|
+
const apiGroup = separator < 0 ? "" : apiVersionValue.slice(0, separator);
|
|
1464
|
+
const apiVersion = separator < 0 ? apiVersionValue : apiVersionValue.slice(separator + 1);
|
|
1465
|
+
const objectNamespace = typeof metadata.namespace === "string" ? metadata.namespace : null;
|
|
1466
|
+
return { apiGroup, apiVersion, kind, namespace: objectNamespace ?? fallbackNamespace, name };
|
|
1467
|
+
}
|
|
1468
|
+
sameIdentity(object, expected) {
|
|
1469
|
+
const actual = this.identity(object, expected.namespace);
|
|
1470
|
+
return actual.apiGroup === expected.apiGroup && actual.apiVersion === expected.apiVersion && actual.kind === expected.kind && actual.namespace === expected.namespace && actual.name === expected.name;
|
|
1471
|
+
}
|
|
1472
|
+
async jsonCommand(args) {
|
|
1473
|
+
const output = await this.runner.run(args);
|
|
1474
|
+
if (Buffer.byteLength(output, "utf8") > 4 * 1024 * 1024) {
|
|
1475
|
+
throw new Error("KUBERNETES_COMMAND_OUTPUT_TOO_LARGE");
|
|
1476
|
+
}
|
|
1477
|
+
try {
|
|
1478
|
+
return JSON.parse(output);
|
|
1479
|
+
} catch {
|
|
1480
|
+
throw new Error("KUBERNETES_COMMAND_OUTPUT_INVALID");
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
objects(input) {
|
|
1484
|
+
const value = this.record(input);
|
|
1485
|
+
if (value.kind === "List" && Array.isArray(value.items)) {
|
|
1486
|
+
return value.items.map((item) => this.record(item)).filter((item) => Object.keys(item).length > 0);
|
|
1487
|
+
}
|
|
1488
|
+
return Object.keys(value).length > 0 ? [value] : [];
|
|
1489
|
+
}
|
|
1490
|
+
record(input) {
|
|
1491
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/kubernetes/KubernetesCommandRunner.ts
|
|
1496
|
+
import { spawn } from "child_process";
|
|
1497
|
+
var KubectlCommandError = class extends Error {
|
|
1498
|
+
constructor(code) {
|
|
1499
|
+
super(code);
|
|
1500
|
+
this.code = code;
|
|
1501
|
+
this.name = "KubectlCommandError";
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var KubectlCommandRunner = class {
|
|
1505
|
+
constructor(context, timeoutMs = 1e4) {
|
|
1506
|
+
this.context = context;
|
|
1507
|
+
this.timeoutMs = timeoutMs;
|
|
1508
|
+
}
|
|
1509
|
+
run(args, standardInput) {
|
|
1510
|
+
if (standardInput && Buffer.byteLength(standardInput, "utf8") > 4 * 1024 * 1024) {
|
|
1511
|
+
return Promise.reject(new KubectlCommandError("KUBECTL_STANDARD_INPUT_TOO_LARGE"));
|
|
1512
|
+
}
|
|
1513
|
+
const fullArgs = this.context ? ["--context", this.context, ...args] : args;
|
|
1514
|
+
return new Promise((resolve, reject) => {
|
|
1515
|
+
const child = spawn("kubectl", fullArgs, {
|
|
1516
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1517
|
+
windowsHide: true
|
|
1518
|
+
});
|
|
1519
|
+
const chunks = [];
|
|
1520
|
+
let outputBytes = 0;
|
|
1521
|
+
let settled = false;
|
|
1522
|
+
const finish = (callback) => {
|
|
1523
|
+
if (settled) return;
|
|
1524
|
+
settled = true;
|
|
1525
|
+
clearTimeout(timer);
|
|
1526
|
+
callback();
|
|
1527
|
+
};
|
|
1528
|
+
const timer = setTimeout(() => {
|
|
1529
|
+
child.kill("SIGKILL");
|
|
1530
|
+
finish(() => reject(new KubectlCommandError("KUBECTL_COMMAND_TIMEOUT")));
|
|
1531
|
+
}, this.timeoutMs);
|
|
1532
|
+
child.stdout.on("data", (chunk) => {
|
|
1533
|
+
outputBytes += chunk.length;
|
|
1534
|
+
if (outputBytes > 4 * 1024 * 1024) {
|
|
1535
|
+
child.kill("SIGKILL");
|
|
1536
|
+
finish(() => reject(new KubectlCommandError("KUBECTL_COMMAND_OUTPUT_TOO_LARGE")));
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
chunks.push(chunk);
|
|
1540
|
+
});
|
|
1541
|
+
child.stderr.resume();
|
|
1542
|
+
child.on("error", () => {
|
|
1543
|
+
finish(() => reject(new KubectlCommandError("KUBECTL_COMMAND_FAILED")));
|
|
1544
|
+
});
|
|
1545
|
+
child.on("close", (code) => {
|
|
1546
|
+
finish(() => {
|
|
1547
|
+
if (code !== 0) {
|
|
1548
|
+
reject(new KubectlCommandError("KUBECTL_COMMAND_FAILED"));
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
child.stdin.on("error", () => void 0);
|
|
1555
|
+
child.stdin.end(standardInput);
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
// ../../packages/dossier-verify/src/canonical.ts
|
|
1561
|
+
function canonicalize(value) {
|
|
1562
|
+
if (Array.isArray(value)) {
|
|
1563
|
+
return value.map((item) => canonicalize(item));
|
|
1564
|
+
}
|
|
1565
|
+
if (!value || typeof value !== "object") {
|
|
1566
|
+
return value;
|
|
1567
|
+
}
|
|
1568
|
+
const record2 = value;
|
|
1569
|
+
const sortedKeys = Object.keys(record2).sort();
|
|
1570
|
+
const normalized = {};
|
|
1571
|
+
for (const key of sortedKeys) {
|
|
1572
|
+
normalized[key] = canonicalize(record2[key]);
|
|
1573
|
+
}
|
|
1574
|
+
return normalized;
|
|
1575
|
+
}
|
|
1576
|
+
function stableJsonStringify2(value) {
|
|
1577
|
+
return JSON.stringify(canonicalize(value));
|
|
1578
|
+
}
|
|
1579
|
+
function jcsCanonicalize(value) {
|
|
1580
|
+
if (value === null) return "null";
|
|
1581
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1582
|
+
if (typeof value === "string") {
|
|
1583
|
+
assertUnicodeScalarString(value);
|
|
1584
|
+
return JSON.stringify(value);
|
|
1585
|
+
}
|
|
1586
|
+
if (typeof value === "number") {
|
|
1587
|
+
if (!Number.isFinite(value)) throw new TypeError("JCS_NUMBER_MUST_BE_FINITE");
|
|
1588
|
+
return JSON.stringify(value);
|
|
1589
|
+
}
|
|
1590
|
+
if (Array.isArray(value)) return `[${value.map((item) => jcsCanonicalize(item)).join(",")}]`;
|
|
1591
|
+
if (!value || typeof value !== "object") throw new TypeError("JCS_VALUE_NOT_JSON");
|
|
1592
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1593
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1594
|
+
throw new TypeError("JCS_OBJECT_MUST_BE_PLAIN");
|
|
1595
|
+
}
|
|
1596
|
+
const record2 = value;
|
|
1597
|
+
return `{${Object.keys(record2).sort().map((key) => {
|
|
1598
|
+
assertUnicodeScalarString(key);
|
|
1599
|
+
if (record2[key] === void 0) throw new TypeError("JCS_UNDEFINED_NOT_ALLOWED");
|
|
1600
|
+
return `${JSON.stringify(key)}:${jcsCanonicalize(record2[key])}`;
|
|
1601
|
+
}).join(",")}}`;
|
|
1602
|
+
}
|
|
1603
|
+
function assertUnicodeScalarString(value) {
|
|
1604
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1605
|
+
const code = value.charCodeAt(index);
|
|
1606
|
+
if (code >= 55296 && code <= 56319) {
|
|
1607
|
+
const next = value.charCodeAt(index + 1);
|
|
1608
|
+
if (!(next >= 56320 && next <= 57343)) throw new TypeError("JCS_LONE_SURROGATE");
|
|
1609
|
+
index += 1;
|
|
1610
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1611
|
+
throw new TypeError("JCS_LONE_SURROGATE");
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// ../../packages/dossier-verify/src/verify.ts
|
|
1617
|
+
import { createHash as createHash3, createPublicKey, verify as verifyEd25519 } from "crypto";
|
|
1618
|
+
function asRecord(value) {
|
|
1619
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1620
|
+
return value;
|
|
1621
|
+
}
|
|
1622
|
+
function asString(value) {
|
|
1623
|
+
return typeof value === "string" ? value : null;
|
|
1624
|
+
}
|
|
1625
|
+
function resolveJsonPointer(root, pointer2) {
|
|
1626
|
+
if (!pointer2.startsWith("/")) return void 0;
|
|
1627
|
+
return pointer2.split("/").slice(1).reduce((current, rawSegment) => {
|
|
1628
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1629
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1630
|
+
return current[segment];
|
|
1631
|
+
}
|
|
1632
|
+
return void 0;
|
|
1633
|
+
}, root);
|
|
1634
|
+
}
|
|
1635
|
+
function findJwk(jwks, keyIds) {
|
|
1636
|
+
const wanted = new Set(keyIds.filter((keyId) => keyId.trim().length > 0));
|
|
1637
|
+
if (!jwks || wanted.size === 0) return null;
|
|
1638
|
+
for (const key of jwks.keys) {
|
|
1639
|
+
if (asString(key.kty) === "OKP" && asString(key.crv) === "Ed25519" && wanted.has(asString(key.kid) ?? "")) {
|
|
1640
|
+
return key;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
function parseProofBundle(value) {
|
|
1646
|
+
const record2 = asRecord(value);
|
|
1647
|
+
if (asString(record2.bundle_type) !== "decionis.decision_dossier.proof_bundle" || asString(record2.version) !== "2.0" && asString(record2.version) !== "2.1" || asString(record2.issued_at) == null || asString(record2.key_id) == null) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
const rotationPolicyRecord = asRecord(record2.rotation_policy);
|
|
1651
|
+
const activeKeyId = asString(rotationPolicyRecord.active_key_id);
|
|
1652
|
+
const publicJwksPath = asString(rotationPolicyRecord.public_jwks_path);
|
|
1653
|
+
if (!activeKeyId || !publicJwksPath) return null;
|
|
1654
|
+
const artifacts = (Array.isArray(record2.artifacts) ? record2.artifacts : []).map((entry) => {
|
|
1655
|
+
const item = asRecord(entry);
|
|
1656
|
+
const artifactKind = asString(item.artifact_kind);
|
|
1657
|
+
const mediaType = asString(item.media_type);
|
|
1658
|
+
const documentPath = asString(item.document_path);
|
|
1659
|
+
const canonicalDocumentSha = asString(item.canonical_document_sha256);
|
|
1660
|
+
const signature = asString(item.signature);
|
|
1661
|
+
const canonicalizationProfile = asString(item.canonicalization_profile);
|
|
1662
|
+
if (!artifactKind || !mediaType || !documentPath || !canonicalDocumentSha || !signature) {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
artifact_kind: artifactKind,
|
|
1667
|
+
media_type: mediaType,
|
|
1668
|
+
document_path: documentPath,
|
|
1669
|
+
canonical_document_sha256: canonicalDocumentSha,
|
|
1670
|
+
...canonicalizationProfile === "RFC8785/JCS" || canonicalizationProfile === "DECIONIS_SORTED_JSON_V1" ? { canonicalization_profile: canonicalizationProfile } : {},
|
|
1671
|
+
signature
|
|
1672
|
+
};
|
|
1673
|
+
}).filter((item) => item !== null);
|
|
1674
|
+
if (artifacts.length === 0) return null;
|
|
1675
|
+
const previousKeyIds = (Array.isArray(rotationPolicyRecord.previous_key_ids) ? rotationPolicyRecord.previous_key_ids : []).map((entry) => asString(entry)).filter((entry) => entry !== null);
|
|
1676
|
+
const gracePeriod = Number.parseInt(
|
|
1677
|
+
String(rotationPolicyRecord.verification_grace_period_days ?? ""),
|
|
1678
|
+
10
|
|
1679
|
+
);
|
|
1680
|
+
return {
|
|
1681
|
+
bundle_type: "decionis.decision_dossier.proof_bundle",
|
|
1682
|
+
version: asString(record2.version),
|
|
1683
|
+
issued_at: asString(record2.issued_at),
|
|
1684
|
+
algorithm: "Ed25519",
|
|
1685
|
+
key_id: asString(record2.key_id),
|
|
1686
|
+
rotation_policy: {
|
|
1687
|
+
strategy: asString(rotationPolicyRecord.strategy) ?? "JWKS_OVERLAP",
|
|
1688
|
+
active_key_id: activeKeyId,
|
|
1689
|
+
previous_key_ids: previousKeyIds,
|
|
1690
|
+
verification_grace_period_days: Number.isFinite(gracePeriod) ? gracePeriod : 0,
|
|
1691
|
+
rotated_at: asString(rotationPolicyRecord.rotated_at),
|
|
1692
|
+
public_jwks_path: publicJwksPath,
|
|
1693
|
+
public_jwks_url: asString(rotationPolicyRecord.public_jwks_url) ?? void 0
|
|
1694
|
+
},
|
|
1695
|
+
artifacts
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function verifyDossierProofBundle(input) {
|
|
1699
|
+
const integrity = asRecord(input.dossier_payload.integrity);
|
|
1700
|
+
const bundle = parseProofBundle(integrity.proof_bundle);
|
|
1701
|
+
if (!bundle) {
|
|
1702
|
+
return {
|
|
1703
|
+
verified: false,
|
|
1704
|
+
available: false,
|
|
1705
|
+
key_id: null,
|
|
1706
|
+
artifacts_checked: 0,
|
|
1707
|
+
checks: [
|
|
1708
|
+
{
|
|
1709
|
+
key: "proof_bundle",
|
|
1710
|
+
label: "Proof Bundle",
|
|
1711
|
+
verified: false,
|
|
1712
|
+
severity: "warn",
|
|
1713
|
+
detail: "No persisted Ed25519 proof bundle was available for verification."
|
|
1714
|
+
}
|
|
1715
|
+
]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
const jwk = findJwk(input.public_jwks ?? null, [
|
|
1719
|
+
bundle.key_id,
|
|
1720
|
+
bundle.rotation_policy.active_key_id,
|
|
1721
|
+
...bundle.rotation_policy.previous_key_ids
|
|
1722
|
+
]);
|
|
1723
|
+
if (!jwk) {
|
|
1724
|
+
return {
|
|
1725
|
+
verified: false,
|
|
1726
|
+
available: true,
|
|
1727
|
+
key_id: bundle.key_id,
|
|
1728
|
+
artifacts_checked: 0,
|
|
1729
|
+
checks: [
|
|
1730
|
+
{
|
|
1731
|
+
key: "public_key",
|
|
1732
|
+
label: "Public Key",
|
|
1733
|
+
verified: false,
|
|
1734
|
+
severity: "fail",
|
|
1735
|
+
detail: `No Ed25519 public key matched proof bundle key ${bundle.key_id}.`
|
|
1736
|
+
}
|
|
1737
|
+
]
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
let publicKey;
|
|
1741
|
+
try {
|
|
1742
|
+
publicKey = createPublicKey({ key: jwk, format: "jwk" });
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
return {
|
|
1745
|
+
verified: false,
|
|
1746
|
+
available: true,
|
|
1747
|
+
key_id: bundle.key_id,
|
|
1748
|
+
artifacts_checked: 0,
|
|
1749
|
+
checks: [
|
|
1750
|
+
{
|
|
1751
|
+
key: "public_key",
|
|
1752
|
+
label: "Public Key",
|
|
1753
|
+
verified: false,
|
|
1754
|
+
severity: "fail",
|
|
1755
|
+
detail: err instanceof Error ? err.message : "Unable to import Ed25519 public key."
|
|
1756
|
+
}
|
|
1757
|
+
]
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
const checks = [
|
|
1761
|
+
{
|
|
1762
|
+
key: "public_key",
|
|
1763
|
+
label: "Public Key",
|
|
1764
|
+
verified: true,
|
|
1765
|
+
severity: "pass",
|
|
1766
|
+
detail: `Loaded Ed25519 public key ${String(jwk.kid ?? bundle.key_id)}.`
|
|
1767
|
+
}
|
|
1768
|
+
];
|
|
1769
|
+
let artifactsChecked = 0;
|
|
1770
|
+
for (const artifact of bundle.artifacts) {
|
|
1771
|
+
const document = resolveJsonPointer(input.dossier_payload, artifact.document_path);
|
|
1772
|
+
if (document === void 0) {
|
|
1773
|
+
checks.push({
|
|
1774
|
+
key: `${artifact.artifact_kind}:document`,
|
|
1775
|
+
label: `${artifact.artifact_kind} Document`,
|
|
1776
|
+
verified: false,
|
|
1777
|
+
severity: "fail",
|
|
1778
|
+
detail: `Signed document path ${artifact.document_path} was not present in the dossier payload.`
|
|
1779
|
+
});
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const canonicalDocument = artifact.canonicalization_profile === "RFC8785/JCS" ? jcsCanonicalize(document) : stableJsonStringify2(document);
|
|
1783
|
+
const actualSha = createHash3("sha256").update(canonicalDocument, "utf8").digest("hex");
|
|
1784
|
+
const hashVerified = actualSha.toLowerCase() === artifact.canonical_document_sha256.toLowerCase();
|
|
1785
|
+
checks.push({
|
|
1786
|
+
key: `${artifact.artifact_kind}:sha256`,
|
|
1787
|
+
label: `${artifact.artifact_kind} SHA-256`,
|
|
1788
|
+
verified: hashVerified,
|
|
1789
|
+
severity: hashVerified ? "pass" : "fail",
|
|
1790
|
+
detail: hashVerified ? `Canonical digest ${actualSha}.` : `Expected ${artifact.canonical_document_sha256}, computed ${actualSha}.`
|
|
1791
|
+
});
|
|
1792
|
+
let signatureVerified = false;
|
|
1793
|
+
try {
|
|
1794
|
+
signatureVerified = verifyEd25519(
|
|
1795
|
+
null,
|
|
1796
|
+
Buffer.from(canonicalDocument, "utf8"),
|
|
1797
|
+
publicKey,
|
|
1798
|
+
Buffer.from(artifact.signature, "base64url")
|
|
1799
|
+
);
|
|
1800
|
+
} catch {
|
|
1801
|
+
signatureVerified = false;
|
|
1802
|
+
}
|
|
1803
|
+
checks.push({
|
|
1804
|
+
key: `${artifact.artifact_kind}:signature`,
|
|
1805
|
+
label: `${artifact.artifact_kind} Signature`,
|
|
1806
|
+
verified: signatureVerified,
|
|
1807
|
+
severity: signatureVerified ? "pass" : "fail",
|
|
1808
|
+
detail: signatureVerified ? "Ed25519 signature verified against the public JWKS." : "Ed25519 signature did not verify against the public JWKS."
|
|
1809
|
+
});
|
|
1810
|
+
artifactsChecked += 1;
|
|
1811
|
+
}
|
|
1812
|
+
return {
|
|
1813
|
+
verified: checks.every((check) => check.verified),
|
|
1814
|
+
available: true,
|
|
1815
|
+
key_id: bundle.key_id,
|
|
1816
|
+
artifacts_checked: artifactsChecked,
|
|
1817
|
+
checks
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// ../../packages/dossier-verify/src/resolve.ts
|
|
1822
|
+
function asRecord2(value) {
|
|
1823
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1824
|
+
return value;
|
|
1825
|
+
}
|
|
1826
|
+
function extractDossierPayload(body) {
|
|
1827
|
+
const record2 = asRecord2(body);
|
|
1828
|
+
if (asRecord2(record2.integrity).proof_bundle !== void 0) return record2;
|
|
1829
|
+
for (const key of ["dossier", "dossier_payload", "data", "result"]) {
|
|
1830
|
+
const candidate = asRecord2(record2[key]);
|
|
1831
|
+
if (asRecord2(candidate.integrity).proof_bundle !== void 0) return candidate;
|
|
1832
|
+
}
|
|
1833
|
+
return record2;
|
|
1834
|
+
}
|
|
1835
|
+
function resolveJwksUrl(payload, dossierUrl) {
|
|
1836
|
+
const bundle = parseProofBundle(asRecord2(payload.integrity).proof_bundle);
|
|
1837
|
+
if (!bundle) return null;
|
|
1838
|
+
const absolute = bundle.rotation_policy.public_jwks_url;
|
|
1839
|
+
if (absolute && /^https?:\/\//i.test(absolute)) return absolute;
|
|
1840
|
+
try {
|
|
1841
|
+
return new URL(bundle.rotation_policy.public_jwks_path, dossierUrl).toString();
|
|
1842
|
+
} catch {
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
var MAX_VERIFICATION_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
1847
|
+
async function fetchJson(url, fetchImpl) {
|
|
1848
|
+
const response = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
1849
|
+
if (!response.ok) {
|
|
1850
|
+
throw new Error(`Request to ${url} failed with HTTP ${response.status}.`);
|
|
1851
|
+
}
|
|
1852
|
+
const responseText = await boundedResponseText(response, url);
|
|
1853
|
+
return JSON.parse(responseText);
|
|
1854
|
+
}
|
|
1855
|
+
async function boundedResponseText(response, url) {
|
|
1856
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
1857
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_VERIFICATION_RESPONSE_BYTES) {
|
|
1858
|
+
throw new Error(`Response from ${url} exceeded the verification size limit.`);
|
|
1859
|
+
}
|
|
1860
|
+
if (!response.body) return "";
|
|
1861
|
+
const reader = response.body.getReader();
|
|
1862
|
+
const chunks = [];
|
|
1863
|
+
let receivedBytes = 0;
|
|
1864
|
+
while (true) {
|
|
1865
|
+
const { done, value } = await reader.read();
|
|
1866
|
+
if (done) break;
|
|
1867
|
+
receivedBytes += value.byteLength;
|
|
1868
|
+
if (receivedBytes > MAX_VERIFICATION_RESPONSE_BYTES) {
|
|
1869
|
+
await reader.cancel().catch(() => void 0);
|
|
1870
|
+
throw new Error(`Response from ${url} exceeded the verification size limit.`);
|
|
1871
|
+
}
|
|
1872
|
+
chunks.push(value);
|
|
1873
|
+
}
|
|
1874
|
+
return Buffer.concat(
|
|
1875
|
+
chunks.map((chunk) => Buffer.from(chunk)),
|
|
1876
|
+
receivedBytes
|
|
1877
|
+
).toString("utf8");
|
|
1878
|
+
}
|
|
1879
|
+
async function verifyDossierFromUrls(input) {
|
|
1880
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
1881
|
+
const body = await fetchJson(input.dossierUrl, fetchImpl);
|
|
1882
|
+
const payload = extractDossierPayload(body);
|
|
1883
|
+
const jwksUrl = input.jwksUrl ?? resolveJwksUrl(payload, input.dossierUrl);
|
|
1884
|
+
if (!jwksUrl) {
|
|
1885
|
+
return {
|
|
1886
|
+
result: verifyDossierProofBundle({ dossier_payload: payload, public_jwks: null }),
|
|
1887
|
+
jwksUrl: null,
|
|
1888
|
+
payload
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
const jwks = await fetchJson(jwksUrl, fetchImpl);
|
|
1892
|
+
return {
|
|
1893
|
+
result: verifyDossierProofBundle({ dossier_payload: payload, public_jwks: jwks }),
|
|
1894
|
+
jwksUrl,
|
|
1895
|
+
payload
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/kubernetes/KubernetesOperations.ts
|
|
1900
|
+
var KubernetesOperations = class _KubernetesOperations {
|
|
1901
|
+
constructor(planner, runner, authority, allowedVerificationOrigins) {
|
|
1902
|
+
this.planner = planner;
|
|
1903
|
+
this.runner = runner;
|
|
1904
|
+
this.authority = authority;
|
|
1905
|
+
this.allowedVerificationOrigins = allowedVerificationOrigins;
|
|
1906
|
+
}
|
|
1907
|
+
async plan(manifestPath, namespace) {
|
|
1908
|
+
return this.planSummary(
|
|
1909
|
+
await this.planner.planManifest(manifestPath, namespace, "mcp:k8s_plan")
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
async requestAuthority(manifestPath, namespace) {
|
|
1913
|
+
const planned = await this.planner.planManifest(
|
|
1914
|
+
manifestPath,
|
|
1915
|
+
namespace,
|
|
1916
|
+
"mcp:k8s_request_authority"
|
|
1917
|
+
);
|
|
1918
|
+
const decisions = await Promise.all(
|
|
1919
|
+
planned.map(async (action) => this.authority.enforceAndBind(action.request))
|
|
1920
|
+
);
|
|
1921
|
+
return decisions.map((decision, index) => this.decisionSummary(decision, planned[index]));
|
|
1922
|
+
}
|
|
1923
|
+
async apply(manifestPath, namespace) {
|
|
1924
|
+
const planned = await this.planner.planManifest(manifestPath, namespace, "mcp:k8s_apply");
|
|
1925
|
+
return this.authorizeAndExecute(planned, async () => {
|
|
1926
|
+
const args = ["apply", "--filename", "-"];
|
|
1927
|
+
if (namespace) args.push("--namespace", namespace);
|
|
1928
|
+
return this.runner.run(args, _KubernetesOperations.executionManifest(planned));
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
async rollback(manifestPath, namespace) {
|
|
1932
|
+
const planned = await this.planner.planManifest(manifestPath, namespace, "mcp:k8s_rollback");
|
|
1933
|
+
if (planned.some((action) => action.operation !== "UPDATE")) {
|
|
1934
|
+
return {
|
|
1935
|
+
executed: false,
|
|
1936
|
+
verdict: "ERROR",
|
|
1937
|
+
decisions: [],
|
|
1938
|
+
reason_code: "KUBERNETES_ROLLBACK_TARGET_NOT_FOUND"
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
return this.authorizeAndExecute(planned, async () => {
|
|
1942
|
+
const args = ["apply", "--filename", "-"];
|
|
1943
|
+
if (namespace) args.push("--namespace", namespace);
|
|
1944
|
+
return this.runner.run(args, _KubernetesOperations.executionManifest(planned));
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
async scale(input) {
|
|
1948
|
+
const planned = await this.planner.planScale(input);
|
|
1949
|
+
if (!planned.executionEndpoint) throw new Error("KUBERNETES_SCALE_ENDPOINT_MISSING");
|
|
1950
|
+
return this.authorizeAndExecute(
|
|
1951
|
+
[planned],
|
|
1952
|
+
() => this.runner.run(
|
|
1953
|
+
["replace", "--raw", planned.executionEndpoint, "--filename", "-"],
|
|
1954
|
+
JSON.stringify(planned.executionObject)
|
|
1955
|
+
)
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
async verifyDossier(dossierUrl, jwksUrl) {
|
|
1959
|
+
this.assertAllowedVerificationUrl(dossierUrl);
|
|
1960
|
+
this.assertAllowedVerificationUrl(jwksUrl);
|
|
1961
|
+
const controller = new AbortController();
|
|
1962
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1963
|
+
try {
|
|
1964
|
+
const verified = await verifyDossierFromUrls({
|
|
1965
|
+
dossierUrl,
|
|
1966
|
+
jwksUrl,
|
|
1967
|
+
fetch: (input, init) => fetch(input, { ...init, redirect: "error", signal: controller.signal })
|
|
1968
|
+
});
|
|
1969
|
+
return {
|
|
1970
|
+
verified: verified.result.verified,
|
|
1971
|
+
available: verified.result.available,
|
|
1972
|
+
key_id: verified.result.key_id,
|
|
1973
|
+
artifacts_checked: verified.result.artifacts_checked,
|
|
1974
|
+
checks: verified.result.checks,
|
|
1975
|
+
jwks_url: verified.jwksUrl
|
|
1976
|
+
};
|
|
1977
|
+
} finally {
|
|
1978
|
+
clearTimeout(timer);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
async authorizeAndExecute(planned, execute) {
|
|
1982
|
+
const decisions = [];
|
|
1983
|
+
for (const action of planned) {
|
|
1984
|
+
try {
|
|
1985
|
+
decisions.push(await this.authority.enforceAndBind(action.request));
|
|
1986
|
+
} catch {
|
|
1987
|
+
return {
|
|
1988
|
+
executed: false,
|
|
1989
|
+
verdict: "ERROR",
|
|
1990
|
+
decisions: decisions.map(
|
|
1991
|
+
(decision, index) => this.decisionSummary(decision, planned[index])
|
|
1992
|
+
),
|
|
1993
|
+
reason_code: "DECIONIS_AUTHORITY_UNAVAILABLE"
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const summaries = decisions.map(
|
|
1998
|
+
(decision, index) => this.decisionSummary(decision, planned[index])
|
|
1999
|
+
);
|
|
2000
|
+
const deniedIndex = decisions.findIndex(
|
|
2001
|
+
(decision, index) => decision.status !== "ALLOW" || !decision.should_execute || !decision.execution_token || decision.mode !== "ENFORCEMENT" || decision.action_hash !== planned[index]?.request.intent_hash || !decision.dossier_id || !decision.dossier_sha256
|
|
2002
|
+
);
|
|
2003
|
+
if (deniedIndex >= 0) {
|
|
2004
|
+
const decision = decisions[deniedIndex];
|
|
2005
|
+
return {
|
|
2006
|
+
executed: false,
|
|
2007
|
+
verdict: _KubernetesOperations.verdict(decision),
|
|
2008
|
+
decisions: summaries,
|
|
2009
|
+
reason_code: decision.reason_codes[0] ?? "DECIONIS_EXECUTION_NOT_AUTHORIZED"
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
for (let index = 0; index < decisions.length; index += 1) {
|
|
2013
|
+
const decision = decisions[index];
|
|
2014
|
+
const action = planned[index];
|
|
2015
|
+
const redemption = await this.authority.redeemGrant(
|
|
2016
|
+
decision.execution_token,
|
|
2017
|
+
action.request,
|
|
2018
|
+
decision
|
|
2019
|
+
);
|
|
2020
|
+
if (!redemption.valid) {
|
|
2021
|
+
return {
|
|
2022
|
+
executed: false,
|
|
2023
|
+
verdict: "ERROR",
|
|
2024
|
+
decisions: summaries,
|
|
2025
|
+
reason_code: redemption.reason_codes[0] ?? "DECIONIS_EXECUTION_GRANT_INVALID"
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
return {
|
|
2031
|
+
executed: true,
|
|
2032
|
+
verdict: "ALLOW",
|
|
2033
|
+
decisions: summaries,
|
|
2034
|
+
kubernetes_result: (await execute()).trim().slice(0, 8e3)
|
|
2035
|
+
};
|
|
2036
|
+
} catch {
|
|
2037
|
+
return {
|
|
2038
|
+
executed: false,
|
|
2039
|
+
verdict: "ERROR",
|
|
2040
|
+
decisions: summaries,
|
|
2041
|
+
reason_code: "KUBERNETES_EXECUTION_FAILED_FRESH_AUTHORITY_REQUIRED"
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
planSummary(planned) {
|
|
2046
|
+
return planned.map((action) => ({
|
|
2047
|
+
action_hash: action.request.intent_hash,
|
|
2048
|
+
action_type: action.request.action.type,
|
|
2049
|
+
operation: action.operation,
|
|
2050
|
+
api_group: action.apiGroup,
|
|
2051
|
+
api_version: action.apiVersion,
|
|
2052
|
+
kind: action.kind,
|
|
2053
|
+
resource: action.resource,
|
|
2054
|
+
namespace: action.namespace,
|
|
2055
|
+
name: action.name,
|
|
2056
|
+
old_object_digest: action.oldObjectDigest,
|
|
2057
|
+
proposed_object_digest: action.proposedObjectDigest,
|
|
2058
|
+
material_diff: action.materialDiff,
|
|
2059
|
+
material_facts: action.materialFacts,
|
|
2060
|
+
note: "No raw manifest or Secret value is included in this plan."
|
|
2061
|
+
}));
|
|
2062
|
+
}
|
|
2063
|
+
decisionSummary(decision, action) {
|
|
2064
|
+
return {
|
|
2065
|
+
verdict: _KubernetesOperations.verdict(decision),
|
|
2066
|
+
decision_id: decision.decision_id,
|
|
2067
|
+
dossier_id: decision.dossier_id,
|
|
2068
|
+
dossier_sha256: decision.dossier_sha256,
|
|
2069
|
+
dossier_url: decision.dossier_url,
|
|
2070
|
+
policy_version: decision.policy_version,
|
|
2071
|
+
approval_request_id: decision.approval_request_id,
|
|
2072
|
+
reason_codes: decision.reason_codes,
|
|
2073
|
+
action_hash: decision.action_hash,
|
|
2074
|
+
expected_action_hash: action.request.intent_hash,
|
|
2075
|
+
resource: action.resource,
|
|
2076
|
+
namespace: action.namespace,
|
|
2077
|
+
name: action.name
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
static verdict(decision) {
|
|
2081
|
+
if (decision.status === "ALLOW") return "ALLOW";
|
|
2082
|
+
if (decision.status === "ESCALATE" || decision.status === "REVIEW_REQUIRED") return "HOLD";
|
|
2083
|
+
if (decision.status === "BLOCK") return "BLOCK";
|
|
2084
|
+
return "ERROR";
|
|
2085
|
+
}
|
|
2086
|
+
static executionManifest(planned) {
|
|
2087
|
+
if (planned.length === 1) return JSON.stringify(planned[0].executionObject);
|
|
2088
|
+
return JSON.stringify({
|
|
2089
|
+
apiVersion: "v1",
|
|
2090
|
+
kind: "List",
|
|
2091
|
+
items: planned.map((action) => action.executionObject)
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
assertAllowedVerificationUrl(input) {
|
|
2095
|
+
const url = new URL(input);
|
|
2096
|
+
if (url.protocol !== "https:" || url.username || url.password || !this.allowedVerificationOrigins.has(url.origin)) {
|
|
2097
|
+
throw new Error("DOSSIER_URL_ORIGIN_NOT_ALLOWED");
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
function createKubernetesAuthorityClient(input) {
|
|
2102
|
+
return createDecionisExecutionClient({
|
|
2103
|
+
authorityBaseUrl: input.baseUrl,
|
|
2104
|
+
timeoutMs: input.timeoutMs,
|
|
2105
|
+
defaultHeaders: { authorization: `Bearer ${input.apiKey}` }
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/kubernetes/KubernetesMcpTools.ts
|
|
2110
|
+
var text2 = (value) => [
|
|
2111
|
+
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
|
|
2112
|
+
];
|
|
2113
|
+
var KubernetesMcpTools = class _KubernetesMcpTools {
|
|
2114
|
+
constructor(operations) {
|
|
2115
|
+
this.operations = operations;
|
|
2116
|
+
}
|
|
2117
|
+
build() {
|
|
2118
|
+
const manifestSchema = {
|
|
2119
|
+
type: "object",
|
|
2120
|
+
required: ["manifest_path"],
|
|
2121
|
+
properties: {
|
|
2122
|
+
manifest_path: {
|
|
2123
|
+
type: "string",
|
|
2124
|
+
description: "Local path to the exact Kubernetes manifest. Manifest content is never returned."
|
|
2125
|
+
},
|
|
2126
|
+
namespace: { type: "string", description: "Optional namespace fallback." }
|
|
2127
|
+
},
|
|
2128
|
+
additionalProperties: false
|
|
2129
|
+
};
|
|
2130
|
+
return [
|
|
2131
|
+
{
|
|
2132
|
+
name: "k8s_plan",
|
|
2133
|
+
title: "Plan Exact Kubernetes Actions",
|
|
2134
|
+
description: "Resolve an exact manifest against the current cluster and return only digests, bounded material facts, and action hashes. Does not call authority or mutate Kubernetes.",
|
|
2135
|
+
inputSchema: manifestSchema,
|
|
2136
|
+
annotations: {
|
|
2137
|
+
readOnlyHint: true,
|
|
2138
|
+
destructiveHint: false,
|
|
2139
|
+
idempotentHint: true,
|
|
2140
|
+
openWorldHint: true
|
|
2141
|
+
},
|
|
2142
|
+
handler: async (args) => ({
|
|
2143
|
+
content: text2(
|
|
2144
|
+
await this.operations.plan(
|
|
2145
|
+
_KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
|
|
2146
|
+
_KubernetesMcpTools.optionalNamespace(args.namespace)
|
|
2147
|
+
)
|
|
2148
|
+
)
|
|
2149
|
+
})
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
name: "k8s_request_authority",
|
|
2153
|
+
title: "Request Kubernetes Execution Authority",
|
|
2154
|
+
description: "Ask the existing Decionis authority for ALLOW, HOLD, or BLOCK on each exact manifest action and return dossier references. Does not run kubectl apply.",
|
|
2155
|
+
inputSchema: manifestSchema,
|
|
2156
|
+
annotations: {
|
|
2157
|
+
readOnlyHint: false,
|
|
2158
|
+
destructiveHint: false,
|
|
2159
|
+
idempotentHint: false,
|
|
2160
|
+
openWorldHint: true
|
|
2161
|
+
},
|
|
2162
|
+
handler: async (args) => ({
|
|
2163
|
+
content: text2(
|
|
2164
|
+
await this.operations.requestAuthority(
|
|
2165
|
+
_KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
|
|
2166
|
+
_KubernetesMcpTools.optionalNamespace(args.namespace)
|
|
2167
|
+
)
|
|
2168
|
+
)
|
|
2169
|
+
})
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
name: "k8s_apply",
|
|
2173
|
+
title: "Apply an Authorized Kubernetes Manifest",
|
|
2174
|
+
description: "Recompute the exact action, require and consume a Decionis ALLOW grant, then run kubectl apply. The admission webhook remains the independent cluster-side gate.",
|
|
2175
|
+
inputSchema: manifestSchema,
|
|
2176
|
+
annotations: {
|
|
2177
|
+
readOnlyHint: false,
|
|
2178
|
+
destructiveHint: true,
|
|
2179
|
+
idempotentHint: false,
|
|
2180
|
+
openWorldHint: true
|
|
2181
|
+
},
|
|
2182
|
+
handler: async (args) => ({
|
|
2183
|
+
content: text2(
|
|
2184
|
+
await this.operations.apply(
|
|
2185
|
+
_KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
|
|
2186
|
+
_KubernetesMcpTools.optionalNamespace(args.namespace)
|
|
2187
|
+
)
|
|
2188
|
+
)
|
|
2189
|
+
})
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
name: "k8s_scale",
|
|
2193
|
+
title: "Scale an Authorized Kubernetes Deployment",
|
|
2194
|
+
description: "Bind the exact Deployment scale object and replica count, require and consume a Decionis ALLOW grant, then run kubectl scale. Scale-to-zero is visible as a material fact.",
|
|
2195
|
+
inputSchema: {
|
|
2196
|
+
type: "object",
|
|
2197
|
+
required: ["namespace", "name", "replicas"],
|
|
2198
|
+
properties: {
|
|
2199
|
+
namespace: { type: "string" },
|
|
2200
|
+
name: { type: "string" },
|
|
2201
|
+
replicas: { type: "integer", minimum: 0, maximum: 1e6 }
|
|
2202
|
+
},
|
|
2203
|
+
additionalProperties: false
|
|
2204
|
+
},
|
|
2205
|
+
annotations: {
|
|
2206
|
+
readOnlyHint: false,
|
|
2207
|
+
destructiveHint: true,
|
|
2208
|
+
idempotentHint: false,
|
|
2209
|
+
openWorldHint: true
|
|
2210
|
+
},
|
|
2211
|
+
handler: async (args) => ({
|
|
2212
|
+
content: text2(
|
|
2213
|
+
await this.operations.scale({
|
|
2214
|
+
namespace: _KubernetesMcpTools.requiredDnsName(args.namespace, "namespace"),
|
|
2215
|
+
name: _KubernetesMcpTools.requiredDnsName(args.name, "name"),
|
|
2216
|
+
replicas: _KubernetesMcpTools.requiredInteger(args.replicas, "replicas")
|
|
2217
|
+
})
|
|
2218
|
+
)
|
|
2219
|
+
})
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
name: "k8s_rollback",
|
|
2223
|
+
title: "Apply an Exact Authorized Rollback",
|
|
2224
|
+
description: "Apply an operator-supplied exact rollback manifest only when the target already exists and Decionis grants the bound UPDATE. Vague rollout undo is intentionally not used.",
|
|
2225
|
+
inputSchema: manifestSchema,
|
|
2226
|
+
annotations: {
|
|
2227
|
+
readOnlyHint: false,
|
|
2228
|
+
destructiveHint: true,
|
|
2229
|
+
idempotentHint: false,
|
|
2230
|
+
openWorldHint: true
|
|
2231
|
+
},
|
|
2232
|
+
handler: async (args) => ({
|
|
2233
|
+
content: text2(
|
|
2234
|
+
await this.operations.rollback(
|
|
2235
|
+
_KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
|
|
2236
|
+
_KubernetesMcpTools.optionalNamespace(args.namespace)
|
|
2237
|
+
)
|
|
2238
|
+
)
|
|
2239
|
+
})
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
name: "k8s_verify_dossier",
|
|
2243
|
+
title: "Verify a Kubernetes Decision Dossier",
|
|
2244
|
+
description: "Fetch a dossier proof bundle and JWKS from configured HTTPS origins and verify SHA-256 plus Ed25519 evidence independently.",
|
|
2245
|
+
inputSchema: {
|
|
2246
|
+
type: "object",
|
|
2247
|
+
required: ["dossier_url", "jwks_url"],
|
|
2248
|
+
properties: {
|
|
2249
|
+
dossier_url: { type: "string", format: "uri" },
|
|
2250
|
+
jwks_url: { type: "string", format: "uri" }
|
|
2251
|
+
},
|
|
2252
|
+
additionalProperties: false
|
|
2253
|
+
},
|
|
2254
|
+
annotations: {
|
|
2255
|
+
readOnlyHint: true,
|
|
2256
|
+
destructiveHint: false,
|
|
2257
|
+
idempotentHint: true,
|
|
2258
|
+
openWorldHint: true
|
|
2259
|
+
},
|
|
2260
|
+
handler: async (args) => ({
|
|
2261
|
+
content: text2(
|
|
2262
|
+
await this.operations.verifyDossier(
|
|
2263
|
+
_KubernetesMcpTools.requiredString(args.dossier_url, "dossier_url"),
|
|
2264
|
+
_KubernetesMcpTools.requiredString(args.jwks_url, "jwks_url")
|
|
2265
|
+
)
|
|
2266
|
+
)
|
|
2267
|
+
})
|
|
312
2268
|
}
|
|
313
2269
|
];
|
|
314
2270
|
}
|
|
2271
|
+
static requiredString(input, field) {
|
|
2272
|
+
if (typeof input !== "string" || !input.trim())
|
|
2273
|
+
throw new Error(`${field.toUpperCase()}_REQUIRED`);
|
|
2274
|
+
return input.trim();
|
|
2275
|
+
}
|
|
2276
|
+
static optionalNamespace(input) {
|
|
2277
|
+
if (input === void 0) return null;
|
|
2278
|
+
return _KubernetesMcpTools.requiredDnsName(input, "namespace");
|
|
2279
|
+
}
|
|
2280
|
+
static requiredDnsName(input, field) {
|
|
2281
|
+
const value = _KubernetesMcpTools.requiredString(input, field);
|
|
2282
|
+
if (!/^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/.test(value) || value.length > 253) {
|
|
2283
|
+
throw new Error(`${field.toUpperCase()}_INVALID`);
|
|
2284
|
+
}
|
|
2285
|
+
return value;
|
|
2286
|
+
}
|
|
2287
|
+
static requiredInteger(input, field) {
|
|
2288
|
+
if (!Number.isInteger(input)) throw new Error(`${field.toUpperCase()}_INVALID`);
|
|
2289
|
+
return input;
|
|
2290
|
+
}
|
|
315
2291
|
};
|
|
316
|
-
function
|
|
317
|
-
|
|
2292
|
+
function createKubernetesMcpToolsFromEnvironment(environment = process.env) {
|
|
2293
|
+
if (environment.DECIONIS_KUBERNETES_TOOLS_ENABLED !== "true") return [];
|
|
2294
|
+
const baseUrl = requiredEnvironment(environment, "DECIONIS_AUTHORITY_BASE_URL");
|
|
2295
|
+
const apiKey = requiredEnvironment(environment, "DECIONIS_API_KEY");
|
|
2296
|
+
const organizationId = requiredEnvironment(environment, "DECIONIS_ORG_ID");
|
|
2297
|
+
const clusterId = requiredEnvironment(environment, "DECIONIS_KUBERNETES_CLUSTER_ID");
|
|
2298
|
+
const actorId = requiredEnvironment(environment, "DECIONIS_KUBERNETES_ACTOR");
|
|
2299
|
+
const policyVersion = requiredEnvironment(environment, "DECIONIS_KUBERNETES_POLICY_VERSION");
|
|
2300
|
+
const context = environment.DECIONIS_KUBERNETES_CONTEXT?.trim() || null;
|
|
2301
|
+
const timeoutMs = boundedInteger(environment.DECIONIS_KUBERNETES_TIMEOUT_MS, 4e3, 100, 1e4);
|
|
2302
|
+
const grantTtlSeconds = boundedInteger(
|
|
2303
|
+
environment.DECIONIS_KUBERNETES_GRANT_TTL_SECONDS,
|
|
2304
|
+
60,
|
|
2305
|
+
1,
|
|
2306
|
+
300
|
|
2307
|
+
);
|
|
2308
|
+
const authorityOrigin = new URL(baseUrl).origin;
|
|
2309
|
+
const allowedVerificationOrigins = /* @__PURE__ */ new Set([
|
|
2310
|
+
authorityOrigin,
|
|
2311
|
+
...(environment.DECIONIS_KUBERNETES_VERIFICATION_ORIGINS ?? "").split(",").map((value) => value.trim()).filter(Boolean).map((value) => new URL(value).origin)
|
|
2312
|
+
]);
|
|
2313
|
+
const runner = new KubectlCommandRunner(context, timeoutMs);
|
|
2314
|
+
const planner = new KubernetesActionPlanner(
|
|
2315
|
+
{ organizationId, clusterId, actorId, policyVersion, grantTtlSeconds },
|
|
2316
|
+
runner
|
|
2317
|
+
);
|
|
2318
|
+
const operations = new KubernetesOperations(
|
|
2319
|
+
planner,
|
|
2320
|
+
runner,
|
|
2321
|
+
createKubernetesAuthorityClient({ baseUrl, apiKey, timeoutMs }),
|
|
2322
|
+
allowedVerificationOrigins
|
|
2323
|
+
);
|
|
2324
|
+
return new KubernetesMcpTools(operations).build();
|
|
2325
|
+
}
|
|
2326
|
+
function requiredEnvironment(environment, name) {
|
|
2327
|
+
const value = environment[name]?.trim();
|
|
2328
|
+
if (!value) throw new Error(`${name}_REQUIRED`);
|
|
2329
|
+
return value;
|
|
2330
|
+
}
|
|
2331
|
+
function boundedInteger(input, fallback, minimum, maximum) {
|
|
2332
|
+
const value = input === void 0 ? fallback : Number(input);
|
|
2333
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
2334
|
+
throw new Error("DECIONIS_KUBERNETES_NUMERIC_CONFIGURATION_INVALID");
|
|
2335
|
+
}
|
|
2336
|
+
return value;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/ShieldClient.ts
|
|
2340
|
+
var ShieldClient = class {
|
|
2341
|
+
baseUrl;
|
|
2342
|
+
apiKey;
|
|
2343
|
+
appId;
|
|
2344
|
+
fetchImpl;
|
|
2345
|
+
constructor(options = {}) {
|
|
2346
|
+
this.baseUrl = (options.baseUrl ?? process.env.DECIONIS_SHIELD_API_URL ?? "https://api.decionis.com").replace(/\/+$/, "");
|
|
2347
|
+
this.apiKey = options.apiKey ?? process.env.DECIONIS_SHIELD_API_KEY ?? "";
|
|
2348
|
+
this.appId = options.appId ?? process.env.DECIONIS_SHIELD_APP_ID ?? "";
|
|
2349
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
2350
|
+
}
|
|
2351
|
+
authorize(request) {
|
|
2352
|
+
return this.json("/v1/shield/authorize", "POST", request);
|
|
2353
|
+
}
|
|
2354
|
+
permission() {
|
|
2355
|
+
return this.json("/v1/shield/permission", "GET");
|
|
2356
|
+
}
|
|
2357
|
+
requestApproval(decisionId) {
|
|
2358
|
+
return this.json(`/v1/shield/decisions/${encodeURIComponent(decisionId)}/approval`, "POST", {});
|
|
2359
|
+
}
|
|
2360
|
+
decision(decisionId) {
|
|
2361
|
+
return this.json(`/v1/shield/decisions/${encodeURIComponent(decisionId)}`, "GET");
|
|
2362
|
+
}
|
|
2363
|
+
async json(path, method, body) {
|
|
2364
|
+
if (!this.apiKey || !this.appId) {
|
|
2365
|
+
throw new ShieldClientError(
|
|
2366
|
+
"SHIELD_MCP_CONFIGURATION_REQUIRED",
|
|
2367
|
+
"Configure DECIONIS_SHIELD_API_KEY and DECIONIS_SHIELD_APP_ID. Do not execute the transaction."
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
2371
|
+
method,
|
|
2372
|
+
headers: {
|
|
2373
|
+
accept: "application/json",
|
|
2374
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
2375
|
+
"content-type": "application/json",
|
|
2376
|
+
"x-shield-app-id": this.appId,
|
|
2377
|
+
"x-shield-environment": "production"
|
|
2378
|
+
},
|
|
2379
|
+
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
2380
|
+
});
|
|
2381
|
+
const payload = await response.json().catch(() => null);
|
|
2382
|
+
if (!response.ok) {
|
|
2383
|
+
const server = asRecord3(payload);
|
|
2384
|
+
throw new ShieldClientError(
|
|
2385
|
+
typeof server.error === "string" ? server.error : "SHIELD_MCP_REQUEST_FAILED",
|
|
2386
|
+
typeof server.message === "string" ? `${server.message} Do not execute the transaction.` : `Shield returned HTTP ${response.status}. Do not execute the transaction.`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
return asRecord3(payload);
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
var ShieldClientError = class extends Error {
|
|
2393
|
+
constructor(code, message) {
|
|
2394
|
+
super(message);
|
|
2395
|
+
this.code = code;
|
|
2396
|
+
this.name = "ShieldClientError";
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
function asRecord3(value) {
|
|
2400
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/ShieldTools.ts
|
|
2404
|
+
var text3 = (value) => [
|
|
2405
|
+
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
|
|
2406
|
+
];
|
|
2407
|
+
var purchaseProperties = {
|
|
2408
|
+
amount: { type: "number", minimum: 0, description: "Amount in major currency units." },
|
|
2409
|
+
currency: {
|
|
2410
|
+
type: "string",
|
|
2411
|
+
pattern: "^[A-Za-z]{3}$",
|
|
2412
|
+
description: "ISO 4217 currency code."
|
|
2413
|
+
},
|
|
2414
|
+
purpose: {
|
|
2415
|
+
type: "string",
|
|
2416
|
+
minLength: 1,
|
|
2417
|
+
description: "Clear user-facing reason for spending."
|
|
2418
|
+
},
|
|
2419
|
+
merchant: { type: "string" },
|
|
2420
|
+
category: { type: "string" },
|
|
2421
|
+
agentId: {
|
|
2422
|
+
type: "string",
|
|
2423
|
+
description: "Registered agent identity, when an agent is spending."
|
|
2424
|
+
},
|
|
2425
|
+
transactionType: {
|
|
2426
|
+
type: "string",
|
|
2427
|
+
enum: ["PURCHASE", "SUBSCRIPTION", "RENEWAL", "TRANSFER", "REFUND", "OTHER"]
|
|
2428
|
+
},
|
|
2429
|
+
metadata: { type: "object" }
|
|
2430
|
+
};
|
|
2431
|
+
var networkMutation = {
|
|
2432
|
+
readOnlyHint: false,
|
|
2433
|
+
destructiveHint: false,
|
|
2434
|
+
idempotentHint: false,
|
|
2435
|
+
openWorldHint: true
|
|
2436
|
+
};
|
|
2437
|
+
var networkRead = {
|
|
2438
|
+
readOnlyHint: true,
|
|
2439
|
+
destructiveHint: false,
|
|
2440
|
+
idempotentHint: true,
|
|
2441
|
+
openWorldHint: true
|
|
2442
|
+
};
|
|
2443
|
+
function createShieldTools(shield) {
|
|
2444
|
+
const authorize = async (args) => {
|
|
2445
|
+
if (typeof args.amount !== "number" || typeof args.currency !== "string" || typeof args.purpose !== "string") {
|
|
2446
|
+
return {
|
|
2447
|
+
content: text3(
|
|
2448
|
+
"amount, currency, and purpose are required. Do not execute the transaction."
|
|
2449
|
+
),
|
|
2450
|
+
isError: true
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
return shieldResult(async () => await shield.authorize(args));
|
|
2454
|
+
};
|
|
2455
|
+
return [
|
|
2456
|
+
{
|
|
2457
|
+
name: "shield.request_purchase",
|
|
2458
|
+
title: "Request Shield Purchase Authorization",
|
|
2459
|
+
description: "Ask the user's Shield for ALLOW, ASK, or BLOCK before a purchase. Execute only on ALLOW.",
|
|
2460
|
+
inputSchema: {
|
|
2461
|
+
type: "object",
|
|
2462
|
+
required: ["amount", "currency", "purpose"],
|
|
2463
|
+
properties: purchaseProperties,
|
|
2464
|
+
additionalProperties: false
|
|
2465
|
+
},
|
|
2466
|
+
annotations: networkMutation,
|
|
2467
|
+
handler: authorize
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
name: "shield.check_spending_authority",
|
|
2471
|
+
title: "Check Shield Spending Authority",
|
|
2472
|
+
description: "Check spending authority for an app or AI agent before taking the consequential action.",
|
|
2473
|
+
inputSchema: {
|
|
2474
|
+
type: "object",
|
|
2475
|
+
required: ["amount", "currency", "purpose"],
|
|
2476
|
+
properties: purchaseProperties,
|
|
2477
|
+
additionalProperties: false
|
|
2478
|
+
},
|
|
2479
|
+
annotations: networkMutation,
|
|
2480
|
+
handler: authorize
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
name: "shield.get_permission",
|
|
2484
|
+
title: "Get Shield Permission",
|
|
2485
|
+
description: "Read the spender identity and user-owned limits bound to this Shield credential.",
|
|
2486
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2487
|
+
annotations: networkRead,
|
|
2488
|
+
handler: async () => shieldResult(async () => await shield.permission())
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
name: "shield.request_approval",
|
|
2492
|
+
title: "Request Shield Approval",
|
|
2493
|
+
description: "Start or poll Presence approval for an ASK decision. Do not execute while the verdict remains ASK.",
|
|
2494
|
+
inputSchema: {
|
|
2495
|
+
type: "object",
|
|
2496
|
+
required: ["decisionId"],
|
|
2497
|
+
properties: { decisionId: { type: "string" } },
|
|
2498
|
+
additionalProperties: false
|
|
2499
|
+
},
|
|
2500
|
+
annotations: networkMutation,
|
|
2501
|
+
handler: async (args) => typeof args.decisionId === "string" ? shieldResult(async () => await shield.requestApproval(args.decisionId)) : {
|
|
2502
|
+
content: text3("decisionId is required. Do not execute the transaction."),
|
|
2503
|
+
isError: true
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
name: "shield.get_decision",
|
|
2508
|
+
title: "Get Shield Decision",
|
|
2509
|
+
description: "Retrieve the current ALLOW, ASK, or BLOCK decision and its evidence identifiers.",
|
|
2510
|
+
inputSchema: {
|
|
2511
|
+
type: "object",
|
|
2512
|
+
required: ["decisionId"],
|
|
2513
|
+
properties: { decisionId: { type: "string" } },
|
|
2514
|
+
additionalProperties: false
|
|
2515
|
+
},
|
|
2516
|
+
annotations: networkRead,
|
|
2517
|
+
handler: async (args) => typeof args.decisionId === "string" ? shieldResult(async () => await shield.decision(args.decisionId)) : {
|
|
2518
|
+
content: text3("decisionId is required. Do not execute the transaction."),
|
|
2519
|
+
isError: true
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
];
|
|
2523
|
+
}
|
|
2524
|
+
async function shieldResult(work) {
|
|
2525
|
+
try {
|
|
2526
|
+
return { content: text3(await work()) };
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
return {
|
|
2529
|
+
content: text3({
|
|
2530
|
+
error: error instanceof Error ? error.name : "ShieldError",
|
|
2531
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2532
|
+
safeToExecute: false
|
|
2533
|
+
}),
|
|
2534
|
+
isError: true
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
318
2537
|
}
|
|
319
2538
|
|
|
320
2539
|
// src/index.ts
|
|
321
2540
|
var gateway = new LocalDecionisGateway();
|
|
2541
|
+
var shieldConfigured = Boolean(
|
|
2542
|
+
process.env.DECIONIS_SHIELD_API_KEY && process.env.DECIONIS_SHIELD_APP_ID
|
|
2543
|
+
);
|
|
322
2544
|
var handler = createMcpHandler({
|
|
323
2545
|
serverInfo: { name: "decionis-mcp", version: MCP_SERVER_VERSION },
|
|
324
|
-
tools: createDecionisTools(gateway
|
|
2546
|
+
tools: createDecionisTools(gateway, [
|
|
2547
|
+
...createKubernetesMcpToolsFromEnvironment(),
|
|
2548
|
+
...shieldConfigured ? createShieldTools(new ShieldClient()) : []
|
|
2549
|
+
])
|
|
325
2550
|
});
|
|
326
2551
|
runStdioServer(handler).then(async () => {
|
|
327
2552
|
await gateway.close();
|