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 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) {
@@ -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)
@@ -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: ${JSON.stringify(subject)}, agentVersion: process.env.AGENT_VERSION ?? "unversioned", framework: ${JSON.stringify(framework)}, adapter: "witnora-init-v0.2" },
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: process.env.AGENT_ENVIRONMENT ?? "development" } },
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
+ }
@@ -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 promotion-receipt.`);
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
- requiredConst(value, "schemaVersion", "agentcert.assurance_scope.v0.1", errors);
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.9.2",
4
- "description": "Witnora evidence, assurance release gates, runtime records, and regression CI for autonomous AI systems.",
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"