witnora 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -52,6 +52,21 @@ runner:
|
|
|
52
52
|
npx witnora@latest setup apply --plan witnora-setup-plan.json --repo .
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Customer-owned deployment boundaries can import the bounded credential broker
|
|
56
|
+
and admission adapters from the canonical package:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import {
|
|
60
|
+
exchangeGitHubOidcPromotionGrant,
|
|
61
|
+
deployWithPromotionEnforcement,
|
|
62
|
+
createKubernetesAdmissionHandler,
|
|
63
|
+
} from "witnora/deployment-enforcement";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The SDK rejects deployment before customer code runs unless the exact artifact,
|
|
67
|
+
target, and adapter match a valid single-use credential. It does not retain raw
|
|
68
|
+
OIDC or deployment bearer tokens.
|
|
69
|
+
|
|
55
70
|
Add `--github-pr` only after reviewing the generated files. The runner accepts
|
|
56
71
|
only the five Witnora templates and ignores commands embedded in a plan.
|
|
57
72
|
Coding, workflow, and data integrations begin at observed-event strength; they
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type DeploymentAdapterKind = "github_environment" | "kubernetes_admission" | "customer_gateway";
|
|
2
|
+
export interface DeploymentCredentialExchangeResult {
|
|
3
|
+
credential: {
|
|
4
|
+
id: string;
|
|
5
|
+
payload: {
|
|
6
|
+
schemaVersion: "witnora.deployment_credential.v0.1";
|
|
7
|
+
artifactDigestSha256: string;
|
|
8
|
+
target: {
|
|
9
|
+
environment: string;
|
|
10
|
+
deploymentUri: string;
|
|
11
|
+
};
|
|
12
|
+
adapters: DeploymentAdapterKind[];
|
|
13
|
+
mode: "PROMOTION" | "ROLLBACK" | "BREAK_GLASS";
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
};
|
|
16
|
+
status: "ISSUED";
|
|
17
|
+
};
|
|
18
|
+
token: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function exchangeGitHubOidcPromotionGrant(input: {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
projectId: string;
|
|
23
|
+
promotionGrantId: string;
|
|
24
|
+
identityProviderId: string;
|
|
25
|
+
brokerApiKey: string;
|
|
26
|
+
audience?: string;
|
|
27
|
+
adapters: DeploymentAdapterKind[];
|
|
28
|
+
rationale: string;
|
|
29
|
+
ttlSeconds?: number;
|
|
30
|
+
idempotencyKey?: string;
|
|
31
|
+
fetch?: typeof fetch;
|
|
32
|
+
environment?: NodeJS.ProcessEnv;
|
|
33
|
+
}): Promise<DeploymentCredentialExchangeResult>;
|
|
34
|
+
export declare function claimDeploymentAdmission(input: {
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
projectId: string;
|
|
37
|
+
credentialId: string;
|
|
38
|
+
deploymentToken: string;
|
|
39
|
+
adapter: DeploymentAdapterKind;
|
|
40
|
+
artifactDigestSha256: string;
|
|
41
|
+
deploymentUri: string;
|
|
42
|
+
idempotencyKey?: string;
|
|
43
|
+
fetch?: typeof fetch;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
admitted: true;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function deployWithPromotionEnforcement<T>(input: {
|
|
48
|
+
admission: Parameters<typeof claimDeploymentAdmission>[0];
|
|
49
|
+
deploy: () => Promise<T>;
|
|
50
|
+
observe: (result: T) => Promise<{
|
|
51
|
+
artifactDigestSha256: string;
|
|
52
|
+
}>;
|
|
53
|
+
rollback?: (result: T) => Promise<void>;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
result: T;
|
|
56
|
+
observedArtifactDigestSha256: string;
|
|
57
|
+
}>;
|
|
58
|
+
export interface KubernetesAdmissionReview {
|
|
59
|
+
apiVersion: "admission.k8s.io/v1";
|
|
60
|
+
kind: "AdmissionReview";
|
|
61
|
+
request?: {
|
|
62
|
+
uid: string;
|
|
63
|
+
object?: {
|
|
64
|
+
metadata?: {
|
|
65
|
+
annotations?: Record<string, string>;
|
|
66
|
+
};
|
|
67
|
+
spec?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export declare function createKubernetesAdmissionHandler(options: {
|
|
72
|
+
baseUrl: string;
|
|
73
|
+
projectId: string;
|
|
74
|
+
deploymentToken: () => string | Promise<string>;
|
|
75
|
+
fetch?: typeof fetch;
|
|
76
|
+
}): (review: KubernetesAdmissionReview) => Promise<Record<string, unknown>>;
|
|
77
|
+
//# sourceMappingURL=deployment-enforcement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-enforcement.d.ts","sourceRoot":"","sources":["../../../../agentcert-sdk/src/deployment-enforcement.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export async function exchangeGitHubOidcPromotionGrant(input) {
|
|
3
|
+
const requestFetch = input.fetch ?? fetch;
|
|
4
|
+
const environment = input.environment ?? process.env;
|
|
5
|
+
const requestUrl = environment.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
6
|
+
const requestToken = environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
7
|
+
if (!requestUrl || !requestToken)
|
|
8
|
+
throw new Error("GitHub Actions OIDC is unavailable. Set permissions: id-token: write.");
|
|
9
|
+
const oidcUrl = new URL(requestUrl);
|
|
10
|
+
oidcUrl.searchParams.set("audience", input.audience ?? "witnora-deployment-broker");
|
|
11
|
+
const oidcResponse = await requestFetch(oidcUrl, { headers: { authorization: `Bearer ${requestToken}`, accept: "application/json" } });
|
|
12
|
+
const oidcBody = await responseObject(oidcResponse);
|
|
13
|
+
if (!oidcResponse.ok || typeof oidcBody.value !== "string")
|
|
14
|
+
throw new Error(message(oidcBody, `GitHub OIDC request failed (${oidcResponse.status}).`));
|
|
15
|
+
const response = await requestFetch(`${stripSlash(input.baseUrl)}/v1/projects/${encodeURIComponent(input.projectId)}/promotion-grants/${encodeURIComponent(input.promotionGrantId)}/exchange-deployment-credential`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { authorization: `Bearer ${input.brokerApiKey}`, "content-type": "application/json" },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
providerId: input.identityProviderId,
|
|
20
|
+
oidcToken: oidcBody.value,
|
|
21
|
+
adapters: input.adapters,
|
|
22
|
+
rationale: input.rationale,
|
|
23
|
+
ttlSeconds: input.ttlSeconds ?? 300,
|
|
24
|
+
idempotencyKey: input.idempotencyKey ?? `${environment.GITHUB_RUN_ID ?? "local"}:${environment.GITHUB_RUN_ATTEMPT ?? "1"}`,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
const body = await responseObject(response);
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(message(body, `Deployment credential exchange failed (${response.status}).`));
|
|
30
|
+
return body;
|
|
31
|
+
}
|
|
32
|
+
export async function claimDeploymentAdmission(input) {
|
|
33
|
+
const response = await (input.fetch ?? fetch)(`${stripSlash(input.baseUrl)}/v1/runtime/projects/${encodeURIComponent(input.projectId)}/deployment-credentials/${encodeURIComponent(input.credentialId)}/admit`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { authorization: `Bearer ${input.deploymentToken}`, "content-type": "application/json" },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
adapter: input.adapter,
|
|
38
|
+
artifactDigestSha256: input.artifactDigestSha256,
|
|
39
|
+
deploymentUri: input.deploymentUri,
|
|
40
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID(),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
const body = await responseObject(response);
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new Error(message(body, `Deployment was denied (${response.status}).`));
|
|
46
|
+
return body;
|
|
47
|
+
}
|
|
48
|
+
export async function deployWithPromotionEnforcement(input) {
|
|
49
|
+
await claimDeploymentAdmission(input.admission);
|
|
50
|
+
const result = await input.deploy();
|
|
51
|
+
try {
|
|
52
|
+
const observation = await input.observe(result);
|
|
53
|
+
if (observation.artifactDigestSha256 !== input.admission.artifactDigestSha256)
|
|
54
|
+
throw new Error("Observed deployment digest does not match the admitted artifact digest.");
|
|
55
|
+
return { result, observedArtifactDigestSha256: observation.artifactDigestSha256 };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (input.rollback)
|
|
59
|
+
await input.rollback(result);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function createKubernetesAdmissionHandler(options) {
|
|
64
|
+
return async (review) => {
|
|
65
|
+
const uid = review.request?.uid ?? "missing-uid";
|
|
66
|
+
const annotations = review.request?.object?.metadata?.annotations ?? {};
|
|
67
|
+
const credentialId = annotations["witnora.com/deployment-credential-id"];
|
|
68
|
+
const artifactDigestSha256 = annotations["witnora.com/artifact-digest-sha256"];
|
|
69
|
+
const deploymentUri = annotations["witnora.com/deployment-uri"];
|
|
70
|
+
if (!credentialId || !artifactDigestSha256 || !deploymentUri)
|
|
71
|
+
return admissionReview(uid, false, "Missing Witnora deployment credential annotations.");
|
|
72
|
+
const images = workloadImages(review.request?.object?.spec);
|
|
73
|
+
if (!images.length || !images.every((image) => image.endsWith(`@sha256:${artifactDigestSha256}`))) {
|
|
74
|
+
return admissionReview(uid, false, "Workload image digest does not match the Witnora admitted artifact digest.");
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await claimDeploymentAdmission({
|
|
78
|
+
baseUrl: options.baseUrl, projectId: options.projectId, credentialId,
|
|
79
|
+
deploymentToken: await options.deploymentToken(), adapter: "kubernetes_admission",
|
|
80
|
+
artifactDigestSha256, deploymentUri, idempotencyKey: `k8s:${uid}`, fetch: options.fetch,
|
|
81
|
+
});
|
|
82
|
+
return admissionReview(uid, true, "Witnora deployment credential admitted the exact artifact and target.");
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return admissionReview(uid, false, error instanceof Error ? error.message : "Witnora deployment admission failed.");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function admissionReview(uid, allowed, messageValue) {
|
|
90
|
+
return { apiVersion: "admission.k8s.io/v1", kind: "AdmissionReview", response: { uid, allowed, status: { message: messageValue } } };
|
|
91
|
+
}
|
|
92
|
+
function workloadImages(spec) {
|
|
93
|
+
const template = object(spec?.template);
|
|
94
|
+
const podSpec = object(template?.spec) ?? spec;
|
|
95
|
+
if (!podSpec)
|
|
96
|
+
return [];
|
|
97
|
+
const containers = [...array(podSpec.containers), ...array(podSpec.initContainers)];
|
|
98
|
+
return containers.flatMap((value) => {
|
|
99
|
+
const container = object(value);
|
|
100
|
+
return typeof container?.image === "string" ? [container.image] : [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : undefined; }
|
|
104
|
+
function array(value) { return Array.isArray(value) ? value : []; }
|
|
105
|
+
async function responseObject(response) {
|
|
106
|
+
return await response.json().catch(() => ({}));
|
|
107
|
+
}
|
|
108
|
+
function message(body, fallback) { return typeof body.error === "string" ? body.error : fallback; }
|
|
109
|
+
function stripSlash(value) { return value.replace(/\/$/, ""); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "witnora",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Witnora evidence, assurance release gates, runtime records, and regression CI for autonomous AI systems.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"./browser-adapter-kit": {
|
|
40
40
|
"types": "./dist/vendor/onegent-runtime/browser-adapter-kit.d.ts",
|
|
41
41
|
"import": "./dist/vendor/onegent-runtime/browser-adapter-kit.js"
|
|
42
|
+
},
|
|
43
|
+
"./deployment-enforcement": {
|
|
44
|
+
"types": "./dist/vendor/agentcert-sdk/deployment-enforcement.d.ts",
|
|
45
|
+
"import": "./dist/vendor/agentcert-sdk/deployment-enforcement.js"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"files": [
|
|
@@ -47,7 +51,7 @@
|
|
|
47
51
|
"package.json"
|
|
48
52
|
],
|
|
49
53
|
"scripts": {
|
|
50
|
-
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.onegent-vendor.json",
|
|
54
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.onegent-vendor.json && tsc -p tsconfig.sdk-vendor.json",
|
|
51
55
|
"prepack": "npm run build",
|
|
52
56
|
"test": "vitest run tests",
|
|
53
57
|
"demo:report": "node dist/cli.js report --mcpbench ../../examples/reports/passing/results.json --out .witnora/demo --subject demo-agent"
|