witnora 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/bundle.js +22 -0
- package/dist/cli.js +14 -0
- package/dist/command-help.js +5 -0
- package/dist/control-plane.js +9 -0
- package/dist/onboarding-templates.js +46 -3
- package/dist/runtime-context.js +75 -0
- package/dist/schema-validator.js +141 -3
- package/dist/vendor/agentcert-sdk/deployment-enforcement.d.ts +77 -0
- package/dist/vendor/agentcert-sdk/deployment-enforcement.d.ts.map +1 -0
- package/dist/vendor/agentcert-sdk/deployment-enforcement.js +109 -0
- package/package.json +7 -3
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
|
|
@@ -84,6 +99,28 @@ npx witnora connect --server https://witnora.com --project your-project-id
|
|
|
84
99
|
npx witnora push --evidence .witnora/latest/agentcert-evidence.json
|
|
85
100
|
```
|
|
86
101
|
|
|
102
|
+
### Automatic agent and environment context
|
|
103
|
+
|
|
104
|
+
`run --push`, `push`, and the generated workflow/coding/data adapter register
|
|
105
|
+
the observed agent automatically. Witnora derives the stable identity and
|
|
106
|
+
version from the command subject, the nearest `package.json`, and the current
|
|
107
|
+
CI commit. It records one canonical environment for every run:
|
|
108
|
+
|
|
109
|
+
- local, development, and test execution becomes `sandbox`;
|
|
110
|
+
- pull-request and preview execution becomes `staging`;
|
|
111
|
+
- tagged releases and recognized production hosts become `production`.
|
|
112
|
+
|
|
113
|
+
This context is run evidence, not a project form the customer must keep in
|
|
114
|
+
sync. A later run with the same Agent ID updates the observed version without
|
|
115
|
+
overwriting reviewed permissions. Explicit `WITNORA_AGENT_ID`,
|
|
116
|
+
`WITNORA_AGENT_VERSION`, and `WITNORA_ENVIRONMENT` values take precedence when
|
|
117
|
+
a repository needs a stable identity that differs from its package metadata.
|
|
118
|
+
Unsupported environment values fail closed instead of being guessed.
|
|
119
|
+
|
|
120
|
+
Useful one-run overrides are `--agent-id`, `--agent-name`, `--agent-version`,
|
|
121
|
+
`--agent-framework`, and `--environment`. Raw prompts, credentials, and tool
|
|
122
|
+
arguments are not needed for this registration.
|
|
123
|
+
|
|
87
124
|
Bind a release, pull-request, or nightly run to an issued continuous assurance
|
|
88
125
|
case by declaring the exact reviewed scope:
|
|
89
126
|
|
package/dist/bundle.js
CHANGED
|
@@ -75,8 +75,30 @@ function evidenceStrengthFor(results) {
|
|
|
75
75
|
level: weakest.level,
|
|
76
76
|
claims: [...new Set(values.flatMap((value) => value.claims))],
|
|
77
77
|
limitations: [...new Set(values.flatMap((value) => value.limitations))],
|
|
78
|
+
...(() => {
|
|
79
|
+
const vectors = values.flatMap((value) => value.trustVector ? [value.trustVector] : []);
|
|
80
|
+
return vectors.length === values.length && vectors.length ? { trustVector: weakestTrustVector(vectors) } : {};
|
|
81
|
+
})(),
|
|
78
82
|
};
|
|
79
83
|
}
|
|
84
|
+
function weakestTrustVector(vectors) {
|
|
85
|
+
const capture = weakest(vectors.map((item) => item.capture), ["self_reported", "collector_recorded", "boundary_observed"]);
|
|
86
|
+
const mediation = weakest(vectors.map((item) => item.mediation), ["none", "advisory", "enforced"]);
|
|
87
|
+
const completeness = weakest(vectors.map((item) => item.completeness), ["unknown", "partial", "target_reconciled"]);
|
|
88
|
+
const attestation = weakest(vectors.map((item) => item.attestation), ["unsigned", "source_signed", "platform_signed", "externally_witnessed"]);
|
|
89
|
+
const review = weakest(vectors.map((item) => item.review), ["none", "internal", "independent_external"]);
|
|
90
|
+
const outcome = vectors.some((item) => item.outcome === "contradicted")
|
|
91
|
+
? "contradicted"
|
|
92
|
+
: vectors.some((item) => item.outcome === "unverified") ? "unverified" : "verified";
|
|
93
|
+
return {
|
|
94
|
+
schemaVersion: "witnora.evidence_trust_vector.v0.1",
|
|
95
|
+
capture, mediation, outcome, completeness, attestation, review,
|
|
96
|
+
limitations: [...new Set(vectors.flatMap((item) => item.limitations))],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function weakest(values, order) {
|
|
100
|
+
return values.reduce((left, right) => order.indexOf(left) <= order.indexOf(right) ? left : right);
|
|
101
|
+
}
|
|
80
102
|
function levelForScore(score, passed) {
|
|
81
103
|
if (!passed) {
|
|
82
104
|
return "No assurance decision";
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { runBrowserAdapterCommand } from "./browser-adapter.js";
|
|
|
28
28
|
import { parseAgentTemplate, starterAdapter, starterGitHubActionWorkflow, starterInstructions, starterProfile, starterTripwireConfig, } from "./onboarding-templates.js";
|
|
29
29
|
import { renderTrySummary, writeTryEvidence } from "./try.js";
|
|
30
30
|
import { applyGuidedSetup } from "./guided-setup.js";
|
|
31
|
+
import { resolveRuntimeContext } from "./runtime-context.js";
|
|
31
32
|
process.on("uncaughtException", reportFatalError);
|
|
32
33
|
process.on("unhandledRejection", reportFatalError);
|
|
33
34
|
const command = process.argv[2] ?? "help";
|
|
@@ -535,6 +536,8 @@ else if (command === "schema") {
|
|
|
535
536
|
witnora schema validate --schema assurance-report --file assurance-report.json
|
|
536
537
|
witnora schema validate --schema assurance-delivery --file assurance-delivery.json
|
|
537
538
|
witnora schema validate --schema evidence-signature --file .witnora/latest/agentcert-evidence.json.sig.json
|
|
539
|
+
witnora schema validate --schema assurance-wallet --file assurance-wallet.json
|
|
540
|
+
witnora schema validate --schema payment-token-reference --file payment-token-reference.json
|
|
538
541
|
`);
|
|
539
542
|
}
|
|
540
543
|
}
|
|
@@ -665,6 +668,17 @@ async function pushHostedEvidence(bundle, bytes, fileName) {
|
|
|
665
668
|
skippedCompanionArtifacts: companions?.skipped,
|
|
666
669
|
assurance,
|
|
667
670
|
verifyContinuousAssurance: Boolean(assurance && (healthOut || requireCurrent)),
|
|
671
|
+
runtimeContext: await resolveRuntimeContext({
|
|
672
|
+
subject: bundle.subject.name,
|
|
673
|
+
framework: readFlag("--agent-framework"),
|
|
674
|
+
env: {
|
|
675
|
+
...process.env,
|
|
676
|
+
...(readFlag("--agent-id") ? { WITNORA_AGENT_ID: readFlag("--agent-id") } : {}),
|
|
677
|
+
...(readFlag("--agent-name") ? { WITNORA_AGENT_NAME: readFlag("--agent-name") } : {}),
|
|
678
|
+
...(readFlag("--agent-version") ? { WITNORA_AGENT_VERSION: readFlag("--agent-version") } : {}),
|
|
679
|
+
...(readFlag("--environment") ? { WITNORA_ENVIRONMENT: readFlag("--environment") } : {}),
|
|
680
|
+
},
|
|
681
|
+
}),
|
|
668
682
|
});
|
|
669
683
|
process.stdout.write(`Hosted run: ${result.runId}\nHosted evidence: ${result.evidenceId}\n`);
|
|
670
684
|
if (companions) {
|
package/dist/command-help.js
CHANGED
|
@@ -59,6 +59,11 @@ Options:
|
|
|
59
59
|
--project <id> Hosted project ID
|
|
60
60
|
--api-key <key> Project API key (prefer WITNORA_API_KEY in CI)
|
|
61
61
|
--external-id <id> Idempotent hosted run ID
|
|
62
|
+
--agent-id <id> Stable observed Agent identity (default: evidence subject)
|
|
63
|
+
--agent-name <name> Human-readable observed Agent name
|
|
64
|
+
--agent-version <v> Version override (default: package or CI commit)
|
|
65
|
+
--agent-framework <f> Framework metadata override
|
|
66
|
+
--environment <env> sandbox, staging, or production
|
|
62
67
|
--assurance-case <id> Issued assurance case to reconcile
|
|
63
68
|
--assurance-scope <p> Declared agent/model/prompt/tools/policy/suite scope JSON
|
|
64
69
|
--assurance-trigger <t> auto, pull_request, release, or nightly (default: auto)
|
package/dist/control-plane.js
CHANGED
|
@@ -75,6 +75,15 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
75
75
|
subject: bundle.subject,
|
|
76
76
|
products: bundle.summary.products,
|
|
77
77
|
},
|
|
78
|
+
...(options.runtimeContext ? {
|
|
79
|
+
agent: {
|
|
80
|
+
externalId: options.runtimeContext.agentId,
|
|
81
|
+
name: options.runtimeContext.agentName,
|
|
82
|
+
version: options.runtimeContext.agentVersion,
|
|
83
|
+
framework: options.runtimeContext.framework,
|
|
84
|
+
},
|
|
85
|
+
environment: options.runtimeContext.environment,
|
|
86
|
+
} : {}),
|
|
78
87
|
assurance: options.assurance,
|
|
79
88
|
}),
|
|
80
89
|
});
|
|
@@ -32,10 +32,15 @@ export function starterAdapter(template, subject) {
|
|
|
32
32
|
const eventType = template === "coding" ? "coding.change.proposed" : template === "workflow" ? "workflow.step.completed" : "data.query.completed";
|
|
33
33
|
return `#!/usr/bin/env node
|
|
34
34
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
35
|
+
import { readFile } from "node:fs/promises";
|
|
35
36
|
|
|
36
37
|
const baseUrl = brandedRequired("BASE_URL").replace(/\\\/$/, "");
|
|
37
38
|
const projectId = brandedRequired("PROJECT_ID");
|
|
38
39
|
const apiKey = brandedRequired("API_KEY");
|
|
40
|
+
const agentId = branded("AGENT_ID") ?? ${JSON.stringify(subject)};
|
|
41
|
+
const agentName = branded("AGENT_NAME") ?? humanize(agentId);
|
|
42
|
+
const agentVersion = await discoverAgentVersion();
|
|
43
|
+
const environment = detectEnvironment();
|
|
39
44
|
const now = new Date().toISOString();
|
|
40
45
|
const traceId = randomBytes(16).toString("hex");
|
|
41
46
|
const spanId = randomBytes(8).toString("hex");
|
|
@@ -44,10 +49,10 @@ const envelope = {
|
|
|
44
49
|
envelopeId: randomUUID(),
|
|
45
50
|
kind: "event",
|
|
46
51
|
occurredAt: now,
|
|
47
|
-
source: { agentId
|
|
48
|
-
run: { externalId: process.env.AGENT_RUN_ID ?? randomUUID(), kind: "custom" },
|
|
52
|
+
source: { agentId, agentName, agentVersion, framework: branded("AGENT_FRAMEWORK") ?? ${JSON.stringify(framework)}, adapter: "witnora-init-v0.3" },
|
|
53
|
+
run: { externalId: process.env.WITNORA_RUN_ID ?? process.env.AGENT_RUN_ID ?? randomUUID(), kind: "custom", environment: environment },
|
|
49
54
|
trace: { traceId, spanId },
|
|
50
|
-
event: { type: ${JSON.stringify(eventType)}, actor: "agent", sequence: 0, attributes: { environment
|
|
55
|
+
event: { type: ${JSON.stringify(eventType)}, actor: "agent", sequence: 0, attributes: { environment } },
|
|
51
56
|
};
|
|
52
57
|
const response = await fetch(\`\${baseUrl}/v1/projects/\${encodeURIComponent(projectId)}/envelopes\`, {
|
|
53
58
|
method: "POST",
|
|
@@ -66,6 +71,44 @@ function brandedRequired(suffix) {
|
|
|
66
71
|
if (!value) throw new Error(\`\${name} is required. Connect the Witnora CLI or set the hosted project variables. Legacy AGENTCERT_* names remain supported.\`);
|
|
67
72
|
return value;
|
|
68
73
|
}
|
|
74
|
+
|
|
75
|
+
function branded(suffix) {
|
|
76
|
+
return process.env[\`WITNORA_\${suffix}\`] ?? process.env[\`AGENTCERT_\${suffix}\`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function humanize(value) {
|
|
80
|
+
const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
|
|
81
|
+
return unscoped.replace(/[-_.]+/g, " ").replace(/\\b\\w/g, (character) => character.toUpperCase()).trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function discoverAgentVersion() {
|
|
85
|
+
const explicit = branded("AGENT_VERSION");
|
|
86
|
+
if (explicit) return explicit;
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(await readFile(new URL("./package.json", import.meta.url), "utf8"));
|
|
89
|
+
if (typeof pkg.version === "string" && pkg.version.trim()) return pkg.version.trim();
|
|
90
|
+
} catch {}
|
|
91
|
+
const commit = process.env.GITHUB_SHA ?? process.env.RENDER_GIT_COMMIT ?? process.env.VERCEL_GIT_COMMIT_SHA;
|
|
92
|
+
return commit ? \`git-\${commit.slice(0, 12)}\` : "local";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectEnvironment() {
|
|
96
|
+
const explicit = branded("ENVIRONMENT") ?? process.env.VERCEL_ENV;
|
|
97
|
+
if (explicit) return normalizeEnvironment(explicit);
|
|
98
|
+
if (["pull_request", "pull_request_target"].includes(process.env.GITHUB_EVENT_NAME ?? "")) return "staging";
|
|
99
|
+
if (process.env.GITHUB_REF_TYPE === "tag") return "production";
|
|
100
|
+
if (process.env.RENDER_SERVICE_ID) return process.env.IS_PULL_REQUEST === "true" ? "staging" : "production";
|
|
101
|
+
if (process.env.NODE_ENV === "production") return "production";
|
|
102
|
+
return "sandbox";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeEnvironment(value) {
|
|
106
|
+
const normalized = value.trim().toLowerCase();
|
|
107
|
+
if (["sandbox", "development", "dev", "test", "local"].includes(normalized)) return "sandbox";
|
|
108
|
+
if (["staging", "stage", "preview", "pull_request", "pr"].includes(normalized)) return "staging";
|
|
109
|
+
if (["production", "prod", "live"].includes(normalized)) return "production";
|
|
110
|
+
throw new Error(\`Unsupported Witnora environment \${JSON.stringify(value)}. Use sandbox, staging, or production.\`);
|
|
111
|
+
}
|
|
69
112
|
`;
|
|
70
113
|
}
|
|
71
114
|
export function starterInstructions(template, subject) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
export async function resolveRuntimeContext(options) {
|
|
4
|
+
const env = options.env ?? process.env;
|
|
5
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
6
|
+
const packageMetadata = await nearestPackageMetadata(cwd);
|
|
7
|
+
const agentId = first(env.WITNORA_AGENT_ID, env.AGENTCERT_AGENT_ID, options.subject, packageMetadata?.name) ?? "witnora-agent";
|
|
8
|
+
const agentName = first(env.WITNORA_AGENT_NAME, env.AGENTCERT_AGENT_NAME) ?? humanizeIdentifier(agentId);
|
|
9
|
+
return {
|
|
10
|
+
agentId,
|
|
11
|
+
agentName,
|
|
12
|
+
agentVersion: first(env.WITNORA_AGENT_VERSION, env.AGENTCERT_AGENT_VERSION, packageMetadata?.version, commitVersion(env), "local"),
|
|
13
|
+
...(first(env.WITNORA_AGENT_FRAMEWORK, env.AGENTCERT_AGENT_FRAMEWORK, options.framework)
|
|
14
|
+
? { framework: first(env.WITNORA_AGENT_FRAMEWORK, env.AGENTCERT_AGENT_FRAMEWORK, options.framework) }
|
|
15
|
+
: {}),
|
|
16
|
+
environment: detectRuntimeEnvironment(env),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function humanizeIdentifier(value) {
|
|
20
|
+
const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
|
|
21
|
+
return unscoped.replace(/[-_.]+/g, " ").replace(/\b\w/g, (character) => character.toUpperCase()).trim();
|
|
22
|
+
}
|
|
23
|
+
export function detectRuntimeEnvironment(env) {
|
|
24
|
+
const explicit = first(env.WITNORA_ENVIRONMENT, env.AGENTCERT_ENVIRONMENT);
|
|
25
|
+
if (explicit)
|
|
26
|
+
return normalizeRuntimeEnvironment(explicit);
|
|
27
|
+
if (env.VERCEL_ENV)
|
|
28
|
+
return normalizeRuntimeEnvironment(env.VERCEL_ENV);
|
|
29
|
+
if (env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target")
|
|
30
|
+
return "staging";
|
|
31
|
+
if (env.GITHUB_REF_TYPE === "tag")
|
|
32
|
+
return "production";
|
|
33
|
+
if (env.RENDER_SERVICE_ID)
|
|
34
|
+
return env.IS_PULL_REQUEST === "true" ? "staging" : "production";
|
|
35
|
+
if (env.NODE_ENV === "production")
|
|
36
|
+
return "production";
|
|
37
|
+
return "sandbox";
|
|
38
|
+
}
|
|
39
|
+
export function normalizeRuntimeEnvironment(value) {
|
|
40
|
+
const normalized = value.trim().toLowerCase();
|
|
41
|
+
if (["sandbox", "development", "dev", "test", "local"].includes(normalized))
|
|
42
|
+
return "sandbox";
|
|
43
|
+
if (["staging", "stage", "preview", "pull_request", "pr"].includes(normalized))
|
|
44
|
+
return "staging";
|
|
45
|
+
if (["production", "prod", "live"].includes(normalized))
|
|
46
|
+
return "production";
|
|
47
|
+
throw new Error(`Unsupported Witnora environment ${JSON.stringify(value)}. Use sandbox, staging, or production.`);
|
|
48
|
+
}
|
|
49
|
+
async function nearestPackageMetadata(start) {
|
|
50
|
+
let current = start;
|
|
51
|
+
while (true) {
|
|
52
|
+
try {
|
|
53
|
+
const value = JSON.parse(await readFile(join(current, "package.json"), "utf8"));
|
|
54
|
+
return { name: text(value.name), version: text(value.version) };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== "ENOENT" && !(error instanceof SyntaxError))
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current)
|
|
62
|
+
return undefined;
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function commitVersion(env) {
|
|
67
|
+
const sha = first(env.GITHUB_SHA, env.RENDER_GIT_COMMIT, env.VERCEL_GIT_COMMIT_SHA);
|
|
68
|
+
return sha ? `git-${sha.slice(0, 12)}` : undefined;
|
|
69
|
+
}
|
|
70
|
+
function first(...values) {
|
|
71
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
|
72
|
+
}
|
|
73
|
+
function text(value) {
|
|
74
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
75
|
+
}
|
package/dist/schema-validator.js
CHANGED
|
@@ -23,10 +23,12 @@ export function parseSchemaId(input) {
|
|
|
23
23
|
value === "evolution-manifest" ||
|
|
24
24
|
value === "permission-diff" ||
|
|
25
25
|
value === "promotion-grant" ||
|
|
26
|
-
value === "promotion-receipt"
|
|
26
|
+
value === "promotion-receipt" ||
|
|
27
|
+
value === "assurance-wallet" ||
|
|
28
|
+
value === "payment-token-reference") {
|
|
27
29
|
return value;
|
|
28
30
|
}
|
|
29
|
-
throw new Error(`Unsupported schema "${value}". Use evidence-bundle, result, corpus-record, failure-review, classifier-eval, monitor-snapshot, robustness-lab, release-gate, assurance-report, assurance-delivery, assurance-scope, evidence-signature, evidence-strength, action-mandate, trusted-action-record, trusted-run-receipt, agent-mutation, evolution-manifest, permission-diff, promotion-grant, or
|
|
31
|
+
throw new Error(`Unsupported schema "${value}". Use evidence-bundle, result, corpus-record, failure-review, classifier-eval, monitor-snapshot, robustness-lab, release-gate, assurance-report, assurance-delivery, assurance-scope, evidence-signature, evidence-strength, action-mandate, trusted-action-record, trusted-run-receipt, agent-mutation, evolution-manifest, permission-diff, promotion-grant, promotion-receipt, assurance-wallet, or payment-token-reference.`);
|
|
30
32
|
}
|
|
31
33
|
export function validateAgentCertSchema(schema, input) {
|
|
32
34
|
const errors = [];
|
|
@@ -78,9 +80,100 @@ export function validateAgentCertSchema(schema, input) {
|
|
|
78
80
|
validatePromotionGrant(value, errors);
|
|
79
81
|
if (schema === "promotion-receipt")
|
|
80
82
|
validatePromotionReceipt(value, errors);
|
|
83
|
+
if (schema === "assurance-wallet")
|
|
84
|
+
validateAssuranceWallet(value, errors);
|
|
85
|
+
if (schema === "payment-token-reference")
|
|
86
|
+
validatePaymentTokenReference(value, errors);
|
|
81
87
|
}
|
|
82
88
|
return { schema, valid: errors.length === 0, errors };
|
|
83
89
|
}
|
|
90
|
+
function validateAssuranceWallet(value, errors) {
|
|
91
|
+
requiredConst(value, "schemaVersion", "witnora.assurance_wallet.v0.1", errors);
|
|
92
|
+
for (const field of ["id", "projectId", "name", "ownerPrincipalId", "baseCurrency", "policyDigestSha256", "status", "createdBy", "createdAt", "updatedAt"])
|
|
93
|
+
requiredString(value, field, errors);
|
|
94
|
+
sha256Field(value, "policyDigestSha256", errors);
|
|
95
|
+
requiredEnum(value, "status", ["ACTIVE", "SUSPENDED", "REVOKED"], errors);
|
|
96
|
+
for (const field of ["createdAt", "updatedAt"])
|
|
97
|
+
validateTimestamp(value[field], field, errors);
|
|
98
|
+
const policy = recordValue(value.policy);
|
|
99
|
+
if (!policy) {
|
|
100
|
+
errors.push("policy must be an object.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
requiredArray(policy, "allowedProviders", errors);
|
|
104
|
+
requiredArray(policy, "allowedEnvironments", errors);
|
|
105
|
+
requiredArray(policy, "allowedCurrencies", errors);
|
|
106
|
+
stringArray(policy.allowedProviders, "policy.allowedProviders", errors);
|
|
107
|
+
stringArray(policy.allowedEnvironments, "policy.allowedEnvironments", errors);
|
|
108
|
+
stringArray(policy.allowedCurrencies, "policy.allowedCurrencies", errors);
|
|
109
|
+
enumArray(policy.allowedProviders, "policy.allowedProviders", ["STRIPE", "CLOUDFLARE"], errors);
|
|
110
|
+
enumArray(policy.allowedEnvironments, "policy.allowedEnvironments", ["SANDBOX", "TEST"], errors);
|
|
111
|
+
currencyArray(policy.allowedCurrencies, "policy.allowedCurrencies", errors);
|
|
112
|
+
currencyField(value, "baseCurrency", errors);
|
|
113
|
+
if (Array.isArray(policy.allowedCurrencies) &&
|
|
114
|
+
(policy.allowedCurrencies.length !== 1 || policy.allowedCurrencies[0] !== value.baseCurrency)) {
|
|
115
|
+
errors.push("policy.allowedCurrencies must contain only baseCurrency in v0.1.");
|
|
116
|
+
}
|
|
117
|
+
requiredNumber(policy, "maxAmountPerAction", errors);
|
|
118
|
+
requiredNumber(policy, "maxAmountPerPeriod", errors);
|
|
119
|
+
requiredNumber(policy, "periodSeconds", errors);
|
|
120
|
+
positiveNumber(policy, "maxAmountPerAction", errors);
|
|
121
|
+
positiveNumber(policy, "maxAmountPerPeriod", errors);
|
|
122
|
+
positiveInteger(policy, "periodSeconds", errors);
|
|
123
|
+
requiredBoolean(policy, "mandateRequired", errors);
|
|
124
|
+
requiredBoolean(policy, "independentOutcomeRequired", errors);
|
|
125
|
+
if (policy.mandateRequired !== true)
|
|
126
|
+
errors.push("policy.mandateRequired must be true.");
|
|
127
|
+
if (policy.independentOutcomeRequired !== true)
|
|
128
|
+
errors.push("policy.independentOutcomeRequired must be true.");
|
|
129
|
+
requiredEnum(policy, "recurringPayments", ["DENY", "REQUIRE_APPROVAL"], errors);
|
|
130
|
+
}
|
|
131
|
+
function validatePaymentTokenReference(value, errors) {
|
|
132
|
+
requiredConst(value, "schemaVersion", "witnora.payment_token_reference.v0.1", errors);
|
|
133
|
+
for (const field of ["id", "projectId", "walletId", "providerAccountId", "tokenType", "tokenSha256", "audience", "currency", "expiresAt", "status", "createdBy", "createdAt", "updatedAt"])
|
|
134
|
+
requiredString(value, field, errors);
|
|
135
|
+
sha256Field(value, "tokenSha256", errors);
|
|
136
|
+
requiredNumber(value, "maxAmount", errors);
|
|
137
|
+
positiveNumber(value, "maxAmount", errors);
|
|
138
|
+
currencyField(value, "currency", errors);
|
|
139
|
+
requiredEnum(value, "provider", ["STRIPE", "CLOUDFLARE"], errors);
|
|
140
|
+
requiredEnum(value, "environment", ["SANDBOX", "TEST"], errors);
|
|
141
|
+
requiredEnum(value, "status", ["AVAILABLE", "BOUND", "CONSUMED", "EXPIRED", "REVOKED"], errors);
|
|
142
|
+
for (const field of ["expiresAt", "createdAt", "updatedAt"])
|
|
143
|
+
validateTimestamp(value[field], field, errors);
|
|
144
|
+
}
|
|
145
|
+
function enumArray(value, path, allowed, errors) {
|
|
146
|
+
if (!Array.isArray(value))
|
|
147
|
+
return;
|
|
148
|
+
for (const [index, item] of value.entries()) {
|
|
149
|
+
if (typeof item === "string" && !allowed.includes(item)) {
|
|
150
|
+
errors.push(`${path}[${index}] must be one of: ${allowed.join(", ")}.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function currencyArray(value, path, errors) {
|
|
155
|
+
if (!Array.isArray(value))
|
|
156
|
+
return;
|
|
157
|
+
for (const [index, item] of value.entries()) {
|
|
158
|
+
if (typeof item === "string" && !/^[A-Z]{3}$/.test(item)) {
|
|
159
|
+
errors.push(`${path}[${index}] must be a three-letter uppercase currency code.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function currencyField(input, key, errors) {
|
|
164
|
+
if (typeof input[key] === "string" && !/^[A-Z]{3}$/.test(input[key])) {
|
|
165
|
+
errors.push(`${key} must be a three-letter uppercase currency code.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function positiveNumber(input, key, errors) {
|
|
169
|
+
if (typeof input[key] === "number" && input[key] <= 0)
|
|
170
|
+
errors.push(`${key} must be greater than zero.`);
|
|
171
|
+
}
|
|
172
|
+
function positiveInteger(input, key, errors) {
|
|
173
|
+
if (typeof input[key] === "number" && (!Number.isInteger(input[key]) || input[key] <= 0)) {
|
|
174
|
+
errors.push(`${key} must be a positive integer.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
84
177
|
function validateEvidenceBundle(value, errors) {
|
|
85
178
|
requiredConst(value, "schemaName", "agentcert.evidence_bundle", errors);
|
|
86
179
|
requiredConst(value, "schemaVersion", AGENTCERT_EVIDENCE_SCHEMA_VERSION, errors);
|
|
@@ -135,6 +228,21 @@ function validateEvidenceStrength(value, errors, prefix = "") {
|
|
|
135
228
|
requiredArray(value, "limitations", errors);
|
|
136
229
|
stringArray(value.claims, `${prefix}claims`, errors);
|
|
137
230
|
stringArray(value.limitations, `${prefix}limitations`, errors);
|
|
231
|
+
const trust = recordValue(value.trustVector);
|
|
232
|
+
if (value.trustVector !== undefined && !trust)
|
|
233
|
+
errors.push(`${prefix}trustVector must be an object.`);
|
|
234
|
+
if (trust)
|
|
235
|
+
validateEvidenceTrustVector(trust, errors, `${prefix}trustVector.`);
|
|
236
|
+
}
|
|
237
|
+
function validateEvidenceTrustVector(value, errors, prefix) {
|
|
238
|
+
requiredConst(value, "schemaVersion", "witnora.evidence_trust_vector.v0.1", errors);
|
|
239
|
+
requiredEnumAt(value, "capture", ["self_reported", "collector_recorded", "boundary_observed"], `${prefix}capture`, errors);
|
|
240
|
+
requiredEnumAt(value, "mediation", ["none", "advisory", "enforced"], `${prefix}mediation`, errors);
|
|
241
|
+
requiredEnumAt(value, "outcome", ["unverified", "verified", "contradicted"], `${prefix}outcome`, errors);
|
|
242
|
+
requiredEnumAt(value, "completeness", ["unknown", "partial", "target_reconciled"], `${prefix}completeness`, errors);
|
|
243
|
+
requiredEnumAt(value, "attestation", ["unsigned", "source_signed", "platform_signed", "externally_witnessed"], `${prefix}attestation`, errors);
|
|
244
|
+
requiredEnumAt(value, "review", ["none", "internal", "independent_external"], `${prefix}review`, errors);
|
|
245
|
+
stringArray(value.limitations, `${prefix}limitations`, errors);
|
|
138
246
|
}
|
|
139
247
|
function validateActionMandate(value, errors) {
|
|
140
248
|
requiredConst(value, "schemaVersion", "agentcert.action_mandate.v0.1", errors);
|
|
@@ -363,7 +471,9 @@ function validateAssuranceContinuity(input, errors) {
|
|
|
363
471
|
}
|
|
364
472
|
}
|
|
365
473
|
function validateAssuranceScope(value, errors) {
|
|
366
|
-
|
|
474
|
+
if (value.schemaVersion !== "agentcert.assurance_scope.v0.1" && value.schemaVersion !== "agentcert.assurance_scope.v0.2") {
|
|
475
|
+
errors.push("schemaVersion must be agentcert.assurance_scope.v0.1 or agentcert.assurance_scope.v0.2.");
|
|
476
|
+
}
|
|
367
477
|
for (const key of ["agent", "model", "prompt", "tools", "policy", "scenarioSuite"])
|
|
368
478
|
requiredObject(value, key, errors);
|
|
369
479
|
const agent = recordValue(value.agent);
|
|
@@ -394,6 +504,34 @@ function validateAssuranceScope(value, errors) {
|
|
|
394
504
|
requiredStringAt(suite, "version", "scenarioSuite.version", errors);
|
|
395
505
|
requiredSha256At(suite, "sha256", "scenarioSuite.sha256", errors);
|
|
396
506
|
}
|
|
507
|
+
if (value.schemaVersion === "agentcert.assurance_scope.v0.2") {
|
|
508
|
+
for (const key of ["runtimeBoundary", "outcomeProbe", "evidenceTrust"])
|
|
509
|
+
requiredObject(value, key, errors);
|
|
510
|
+
const runtime = recordValue(value.runtimeBoundary);
|
|
511
|
+
const probe = recordValue(value.outcomeProbe);
|
|
512
|
+
const trust = recordValue(value.evidenceTrust);
|
|
513
|
+
if (runtime) {
|
|
514
|
+
requiredSha256At(runtime, "gatewayDigestSha256", "runtimeBoundary.gatewayDigestSha256", errors);
|
|
515
|
+
requiredSha256At(runtime, "adapterDigestSha256", "runtimeBoundary.adapterDigestSha256", errors);
|
|
516
|
+
requiredStringAt(runtime, "credentialBoundary", "runtimeBoundary.credentialBoundary", errors);
|
|
517
|
+
requiredSha256At(runtime, "networkBoundarySha256", "runtimeBoundary.networkBoundarySha256", errors);
|
|
518
|
+
requiredStringAt(runtime, "grantIssuer", "runtimeBoundary.grantIssuer", errors);
|
|
519
|
+
}
|
|
520
|
+
if (probe) {
|
|
521
|
+
requiredStringAt(probe, "profileId", "outcomeProbe.profileId", errors);
|
|
522
|
+
requiredStringAt(probe, "version", "outcomeProbe.version", errors);
|
|
523
|
+
requiredSha256At(probe, "digestSha256", "outcomeProbe.digestSha256", errors);
|
|
524
|
+
requiredStringAt(probe, "authoritativeSource", "outcomeProbe.authoritativeSource", errors);
|
|
525
|
+
requiredStringAt(probe, "readCredentialBoundary", "outcomeProbe.readCredentialBoundary", errors);
|
|
526
|
+
}
|
|
527
|
+
if (trust) {
|
|
528
|
+
requiredSha256At(trust, "collectorDigestSha256", "evidenceTrust.collectorDigestSha256", errors);
|
|
529
|
+
requiredStringAt(trust, "evidenceSchemaVersion", "evidenceTrust.evidenceSchemaVersion", errors);
|
|
530
|
+
requiredStringAt(trust, "signingKeyId", "evidenceTrust.signingKeyId", errors);
|
|
531
|
+
requiredSha256At(trust, "verifierDigestSha256", "evidenceTrust.verifierDigestSha256", errors);
|
|
532
|
+
requiredSha256At(trust, "retentionPolicySha256", "evidenceTrust.retentionPolicySha256", errors);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
397
535
|
}
|
|
398
536
|
function requiredSha256At(value, key, path, errors) {
|
|
399
537
|
if (typeof value[key] !== "string" || !/^[0-9a-f]{64}$/.test(value[key]))
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "witnora",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Independent assurance for covered agent action paths across models and frameworks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"homepage": "https://witnora.com/",
|
|
@@ -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"
|