witnora 0.8.1 → 0.9.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.
Files changed (87) hide show
  1. package/README.md +275 -12
  2. package/dist/artifact-manifest.js +37 -0
  3. package/dist/artifact-validation.js +102 -0
  4. package/dist/badge.js +31 -0
  5. package/dist/browser-adapter.js +156 -0
  6. package/dist/bundle.js +100 -0
  7. package/dist/cli.js +971 -0
  8. package/dist/command-help.js +71 -0
  9. package/dist/companion-artifacts.js +170 -0
  10. package/dist/conformance.js +115 -0
  11. package/dist/control-plane.js +409 -0
  12. package/dist/corpus-governance.js +126 -0
  13. package/dist/corpus-store.js +239 -0
  14. package/dist/corpus.js +438 -0
  15. package/dist/credentials.js +127 -0
  16. package/dist/evidence-signing.js +85 -0
  17. package/dist/failure-review.js +188 -0
  18. package/dist/guided-setup.js +101 -0
  19. package/dist/index.js +16 -0
  20. package/dist/lab.js +323 -0
  21. package/dist/local-server.js +491 -0
  22. package/dist/monitor.js +100 -0
  23. package/dist/normalizers.js +265 -0
  24. package/dist/onboarding-templates.js +158 -0
  25. package/dist/release-gate.js +457 -0
  26. package/dist/report.js +165 -0
  27. package/dist/runner.js +349 -0
  28. package/dist/sandbox.js +424 -0
  29. package/dist/schema-validator.js +503 -0
  30. package/dist/try.js +135 -0
  31. package/dist/types.js +3 -0
  32. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
  33. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
  34. package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
  36. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
  37. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
  38. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
  39. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
  40. package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
  41. package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
  42. package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
  43. package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
  44. package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
  45. package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
  46. package/dist/vendor/onegent-runtime/policies.js +109 -0
  47. package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
  48. package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
  49. package/dist/vendor/onegent-runtime/risk.js +54 -0
  50. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
  51. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
  52. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
  53. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
  54. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
  55. package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
  56. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
  57. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
  58. package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
  59. package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
  60. package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
  61. package/dist/vendor/onegent-runtime/sdk.js +140 -0
  62. package/dist/vendor/onegent-runtime/service.d.ts +27 -0
  63. package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
  64. package/dist/vendor/onegent-runtime/service.js +423 -0
  65. package/dist/vendor/onegent-runtime/store.d.ts +16 -0
  66. package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
  67. package/dist/vendor/onegent-runtime/store.js +29 -0
  68. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
  69. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
  70. package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
  71. package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
  72. package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
  73. package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
  74. package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
  75. package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
  76. package/dist/vendor/onegent-runtime/trust-types.js +7 -0
  77. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
  78. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
  79. package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
  80. package/dist/vendor/onegent-runtime/types.d.ts +287 -0
  81. package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
  82. package/dist/vendor/onegent-runtime/types.js +1 -0
  83. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
  84. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
  85. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
  86. package/package.json +33 -13
  87. package/bin/witnora.js +0 -3
package/README.md CHANGED
@@ -1,23 +1,286 @@
1
1
  # Witnora CLI
2
2
 
3
- `witnora` is the canonical npm entry point for the Witnora CLI.
3
+ Unified release assurance, evidence, corpus, monitor, and lab CLI for Witnora.
4
+
5
+ Witnora checks what an agent may do, whether it passed pre-release evidence,
6
+ whether a high-risk runtime action may proceed, and who can verify the observed
7
+ outcome. It writes portable reports and accumulates a local failure corpus.
8
+
9
+ [Public evidence](https://witnora.com/evidence) |
10
+ [Get started](https://witnora.com/docs) |
11
+ [Private workspace](https://witnora.com/app) |
12
+ [GitHub source](https://github.com/Kakarottoooo/agentcert)
13
+
14
+ ## 5-minute local path
15
+
16
+ No agent repository or API key is required for the first run:
17
+
18
+ ```bash
19
+ npx witnora@latest try --template workflow
20
+ ```
21
+
22
+ Inspect `.witnora/try/agentcert-report.html` and
23
+ `.witnora/try/agentcert-evidence.json`. The output is explicitly synthetic and
24
+ reported-strength: it proves the evidence tooling works, not that a real agent
25
+ is reliable.
26
+
27
+ When you have an agent repository:
28
+
29
+ ```bash
30
+ npx witnora init --template browser --subject my-browser-agent
31
+ ```
32
+
33
+ This writes `witnora.config.json` and `tripwire.yml`. Add `--github-action`
34
+ when you also want `.github/workflows/witnora-tripwire.yml`.
35
+
36
+ Use the same entry point for other agent boundaries:
37
+
38
+ ```bash
39
+ npx witnora init --template coding
40
+ npx witnora init --template mcp
41
+ npx witnora init --template workflow
42
+ npx witnora init --template data
43
+ ```
44
+
45
+ Coding, workflow, and data templates write a dependency-free Universal
46
+ Event/Action Envelope adapter. MCP writes an MCPBench artifact profile.
47
+
48
+ Hosted guided setup can export a redacted typed plan for the customer-owned
49
+ runner:
50
+
51
+ ```bash
52
+ npx witnora@latest setup apply --plan witnora-setup-plan.json --repo .
53
+ ```
54
+
55
+ Add `--github-pr` only after reviewing the generated files. The runner accepts
56
+ only the five Witnora templates and ignores commands embedded in a plan.
57
+ Coding, workflow, and data integrations begin at observed-event strength; they
58
+ do not establish `CURRENT` until a deterministic task assertion and outcome
59
+ verifier are attached.
60
+
61
+ Edit `tripwire.yml` so `startUrl` and `agent.command` point at your app and
62
+ browser agent. After Tripwire has produced `.tripwire/latest/tripwire-result.json`,
63
+ build the Witnora outputs:
64
+
65
+ ```bash
66
+ npx witnora run --tripwire .tripwire/latest/tripwire-result.json --subject my-browser-agent --fail-on-verdict
67
+ ```
68
+
69
+ Default outputs:
70
+
71
+ - `.witnora/latest/agentcert-evidence.json`
72
+ - `.witnora/latest/agentcert-report.md`
73
+ - `.witnora/latest/agentcert-report.html`
74
+ - `.witnora/latest/agentcert-run-manifest.json`
75
+ - `.witnora/latest/badge.svg`
76
+ - `.witnora/corpus/corpus.jsonl`
77
+ - `.witnora/latest/reviewed-failure-dataset.jsonl`
78
+ - `.witnora/latest/monitor.json`
79
+
80
+ Push the validated evidence bundle into a hosted Witnora project:
81
+
82
+ ```bash
83
+ npx witnora connect --server https://witnora.com --project your-project-id
84
+ npx witnora push --evidence .witnora/latest/agentcert-evidence.json
85
+ ```
86
+
87
+ Bind a release, pull-request, or nightly run to an issued continuous assurance
88
+ case by declaring the exact reviewed scope:
89
+
90
+ ```bash
91
+ npx witnora run --config witnora.config.json --push \
92
+ --assurance-case "$WITNORA_ASSURANCE_CASE_ID" \
93
+ --assurance-scope agentcert.assurance-scope.json \
94
+ --assurance-trigger auto \
95
+ --require-current auto \
96
+ --continuous-health-out .witnora/canary/generated-kit-health.json
97
+ ```
98
+
99
+ Validate the scope before CI uses it:
100
+
101
+ ```bash
102
+ npx witnora schema validate \
103
+ --schema assurance-scope \
104
+ --file agentcert.assurance-scope.json
105
+ ```
106
+
107
+ `auto` treats pull requests as prospective, scheduled workflows as nightly,
108
+ and other GitHub runs as release checks. Authoritative failure or scope drift
109
+ sets the Hosted contract to `REVALIDATION_REQUIRED`; only an independently
110
+ issued successor case establishes a new `CURRENT` baseline.
111
+ Release and nightly checks fail unless Hosted returns `CURRENT`. The optional
112
+ health output is redacted and captures the Hosted run/evidence identifiers,
113
+ evidence completeness, freshness transition, and install-to-CURRENT timing for
114
+ external canaries and operational dashboards.
115
+
116
+ Add `--push` to `witnora run` to run locally and upload the resulting bundle
117
+ in one command. By default, both commands also upload local files referenced by
118
+ the bundle. Reads are confined to `--artifact-root` (the current directory by
119
+ default), path and symlink escapes are rejected, and uploads are capped at 25
120
+ files, 10 MiB per file, and 50 MiB total. Skipped references are reported in
121
+ the CLI and hosted run timeline. Companion uploads are restricted to
122
+ PNG/JPEG/WebP, JSON/JSONL, HTML, PDF, and ZIP; other extensions are skipped
123
+ before they are read. Pass `--no-artifacts` to upload only the JSON bundle.
124
+ Hosted pushes embed an `agentcert.artifact_manifest.v0.1` declaration with the
125
+ normalized path, SHA-256 digest, byte size, and kind of every prepared
126
+ companion artifact. The control plane reports `complete` only after every
127
+ hosted object exactly matches that declaration; missing, skipped, undeclared,
128
+ or mismatched artifacts remain observable as `partial` or `rejected`.
129
+ Project API keys can create runs, record events, and upload evidence, but
130
+ cannot approve their own runtime actions.
131
+
132
+ ## Customer-owned browser gateway
133
+
134
+ Generate and certify a sandbox-only credential-isolated browser adapter:
135
+
136
+ ```bash
137
+ npx witnora browser-adapter init
138
+ npx witnora browser-adapter certify --adapter ./agentcert.browser-adapter.mjs
139
+ ```
140
+
141
+ The adapter imports `agentcert/browser-adapter-kit`, keeps write and read-only
142
+ credentials in separate customer callbacks, and writes a digest-only report.
143
+ It rejects production actions. See the
144
+ [two-hour onboarding guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/customer-owned-browser-adapter.md).
145
+
146
+ ## Sandbox onboarding
147
+
148
+ Create and certify a synthetic SandboxSystem adapter with the same public CLI:
149
+
150
+ ```bash
151
+ npx witnora sandbox init
152
+ npx witnora sandbox certify --adapter ./agentcert.sandbox.mjs
153
+ ```
154
+
155
+ The first command writes one dependency-free JavaScript file. The second runs
156
+ the bundled deterministic adapter contract and writes
157
+ `.witnora/sandbox/sandbox-adapter-conformance.json`. No `@agentcert` scoped
158
+ package is required.
159
+
160
+ After `agentcert connect`, certify and upload in one command:
161
+
162
+ ```bash
163
+ npx witnora sandbox push --adapter ./agentcert.sandbox.mjs
164
+ ```
165
+
166
+ Passing and failing reports are both retained for review, while failed
167
+ certifications return a non-zero exit status. The generated template is
168
+ synthetic and network-denied; it must not contain production credentials or
169
+ connect to live systems.
170
+
171
+ Run one bounded read against an existing Stripe sandbox PaymentIntent:
172
+
173
+ ```bash
174
+ STRIPE_RESTRICTED_TEST_KEY="rk_test_..." npx witnora sandbox stripe-readonly --payment-intent pi_...
175
+ ```
176
+
177
+ Add `--push` to retain the redacted report in Witnora Hosted. The command
178
+ allows only `https://api.stripe.com`, `GET`, and allowlisted PaymentIntent
179
+ routes, with a 5-second timeout and 10-request-per-minute process-local cap.
180
+ The Stripe key is environment-only and is never written to output or evidence.
181
+ See the [Bounded Vendor Sandbox Egress guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/bounded-vendor-sandbox-egress.md).
182
+
183
+ Release maintainers can run the protected, manual
184
+ `Real Stripe sandbox acceptance` GitHub workflow. It performs the bounded read,
185
+ independently scans the generated report before upload, validates it again at
186
+ the CLI boundary, uploads the v0.4 evidence, and compares it with prior
187
+ protected runs. See the [real vendor acceptance guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/real-vendor-acceptance.md).
188
+
189
+ The workflow's upload step is also available as a narrow command for an
190
+ already-generated report:
4
191
 
5
192
  ```bash
6
- npx witnora try --template workflow
193
+ npx witnora sandbox upload-report --report .witnora/vendor-sandbox/current-report.json --external-id vendor-acceptance:stripe:<run>:<attempt>
7
194
  ```
8
195
 
9
- This package is intentionally a thin, version-locked bridge to `agentcert`,
10
- the historical package name. Both commands execute the same maintained CLI:
196
+ This command accepts only Witnora sandbox conformance and vendor-egress
197
+ contracts and rejects reports containing credential-shaped values or forbidden
198
+ sensitive fields.
199
+
200
+ Review/export helpers:
11
201
 
12
202
  ```bash
13
- npx witnora --help
14
- npx --package agentcert witnora --help
15
- npx agentcert --help
203
+ npx witnora corpus metrics --corpus .witnora/corpus/corpus.jsonl
204
+ npx witnora corpus export-reviewed --corpus .witnora/corpus/corpus.jsonl --out .witnora/latest/reviewed-failure-dataset.jsonl
205
+ npx witnora corpus classifier-eval --corpus .witnora/corpus/corpus.jsonl --out .witnora/latest/failure-classifier-evaluation.json
206
+ npx witnora validate .witnora/latest/agentcert-evidence.json
207
+ npx witnora validate .witnora/latest/agentcert-evidence.json --check-artifacts
208
+ npx witnora release-gate --config witnora.config.json --strict
209
+ npx witnora release-gate --evidence .witnora/latest/agentcert-evidence.json --baseline .witnora/baselines/main.json
210
+ npx witnora schema validate --schema evidence-bundle --file .witnora/latest/agentcert-evidence.json
211
+ npx witnora schema validate --schema classifier-eval --file examples/agentcert/classifier-eval.example.json
16
212
  ```
17
213
 
18
- The legacy package and command remain supported during the migration. New
19
- integrations should use `witnora`, `WITNORA_*` environment variables, and
20
- [witnora.com](https://witnora.com/).
214
+ The release gate writes fixed JSON, HTML, Markdown, JUnit, and badge outputs,
215
+ records SHA-256 artifact provenance, and supports optional Ed25519 signatures:
216
+
217
+ ```bash
218
+ npx witnora evidence keygen --private-key .witnora/keys/evidence-private.pem --public-key .witnora/keys/evidence-public.pem
219
+ npx witnora evidence sign .witnora/latest/agentcert-evidence.json --private-key .witnora/keys/evidence-private.pem
220
+ ```
21
221
 
22
- Witnora provides evidence and assurance for autonomous AI systems. It does
23
- not claim to be an official certification authority.
222
+ Control semantics and attestation format:
223
+ [release gate checklist](https://github.com/Kakarottoooo/agentcert/blob/main/docs/release-gate-checklist.md).
224
+
225
+ CI users can run Tripwire and Witnora together with
226
+ `Kakarottoooo/agentcert/actions/tripwire@v0`.
227
+
228
+ The public Real Agent Robustness Lab compares browser-use, Stagehand, and
229
+ Playwright-based agents over the same fault suite:
230
+
231
+ https://kakarottoooo.github.io/agentcert/public-demo/real-agent-robustness/
232
+
233
+ Minimal no-key browser-agent example in the GitHub repo:
234
+
235
+ ```text
236
+ examples/minimal-browser-agent/
237
+ ```
238
+
239
+ External integration smoke matrix in the GitHub repo:
240
+
241
+ ```text
242
+ examples/real-agents/external-integration-smokes.md
243
+ ```
244
+
245
+ Corpus storage:
246
+
247
+ ```bash
248
+ npx witnora corpus ingest --tripwire .tripwire/latest/tripwire-result.json --out .witnora/corpus/corpus.jsonl --subject my-browser-agent
249
+ npx witnora corpus ingest --store sqlite --sqlite .witnora/corpus/agentcert.sqlite --tripwire .tripwire/latest/tripwire-result.json --subject my-browser-agent
250
+ npx witnora monitor build --store postgres --database-url "$WITNORA_DATABASE_URL" --out .witnora/latest/monitor.json --subject my-browser-agent
251
+ ```
252
+
253
+ The UI always reads `agentcert.monitor_snapshot`, so switching from JSONL to
254
+ SQLite or Postgres does not require a frontend rewrite.
255
+
256
+ Failure taxonomy reviews:
257
+
258
+ ```bash
259
+ node packages/witnora-cli/dist/cli.js corpus review \
260
+ --corpus .witnora/corpus/corpus.jsonl \
261
+ --reviews .witnora/corpus/failure-reviews.jsonl \
262
+ --pattern-key "tripwire:network_failure:http-failure:no_console_error" \
263
+ --type console_error \
264
+ --status corrected \
265
+ --reviewer qa@example.com \
266
+ --confidence 0.85 \
267
+ --first-divergence "Console displayed a 503 failure before the task completed." \
268
+ --screenshot "runs/http-failure/step-2.png" \
269
+ --trace "runs/http-failure/trace.json" \
270
+ --why "The failed assertion is about a browser console error." \
271
+ --signal "assertion type no_console_error"
272
+ ```
273
+
274
+ The review command appends an `agentcert.failure_review` JSONL record, reapplies
275
+ the review ledger, and writes the corrected taxonomy back to the corpus store.
276
+ Optional review metadata includes confidence, first-divergence snippets,
277
+ screenshot/trace pointers, supporting signals, classifier limitations, and a
278
+ structured taxonomy rationale for later classifier training and evaluation.
279
+
280
+ For local development inside this repository:
281
+
282
+ ```bash
283
+ npm --prefix packages/witnora-cli ci
284
+ npm --prefix packages/witnora-cli run build
285
+ node packages/witnora-cli/dist/cli.js run --profile public-demo
286
+ ```
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AGENTCERT_ARTIFACT_MANIFEST_VERSION, } from "./types.js";
3
+ export function buildArtifactManifest(artifacts) {
4
+ const entries = artifacts.map((artifact) => ({
5
+ path: normalizeManifestPath(artifact.sourcePath),
6
+ sha256: createHash("sha256").update(artifact.bytes).digest("hex"),
7
+ sizeBytes: artifact.bytes.byteLength,
8
+ kind: artifact.kind,
9
+ })).sort((left, right) => left.path.localeCompare(right.path));
10
+ const paths = new Set();
11
+ for (const entry of entries) {
12
+ if (paths.has(entry.path))
13
+ throw new Error(`Artifact manifest contains duplicate path ${entry.path}.`);
14
+ paths.add(entry.path);
15
+ }
16
+ return { schemaVersion: AGENTCERT_ARTIFACT_MANIFEST_VERSION, entries };
17
+ }
18
+ export function withArtifactManifest(bundle, artifacts) {
19
+ return { ...bundle, artifactManifest: buildArtifactManifest(artifacts) };
20
+ }
21
+ export function serializeHostedEvidenceBundle(bundle, artifacts) {
22
+ const reconciledBundle = withArtifactManifest(bundle, artifacts);
23
+ return {
24
+ bundle: reconciledBundle,
25
+ bytes: new TextEncoder().encode(`${JSON.stringify(reconciledBundle, null, 2)}\n`),
26
+ };
27
+ }
28
+ export function normalizeManifestPath(value) {
29
+ const path = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
30
+ if (!path || path.length > 1024 || path.startsWith("/") || /^[A-Za-z]:\//.test(path)) {
31
+ throw new Error(`Artifact manifest path must be a relative path: ${value}`);
32
+ }
33
+ if (path.split("/").some((segment) => segment === ".." || segment === "")) {
34
+ throw new Error(`Artifact manifest path cannot contain empty or parent segments: ${value}`);
35
+ }
36
+ return path;
37
+ }
@@ -0,0 +1,102 @@
1
+ import { access } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ export async function validateEvidenceArtifacts(input, artifactRoot) {
4
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
5
+ return { checked: 0, missing: [] };
6
+ }
7
+ const uniquePaths = collectEvidenceArtifactPaths(input, artifactRoot)
8
+ .filter((entry) => !isRemoteArtifactPath(entry.sourcePath));
9
+ const missing = [];
10
+ for (const entry of uniquePaths) {
11
+ const fullPath = isAbsolute(entry.sourcePath) ? entry.sourcePath : resolve(entry.root, entry.sourcePath);
12
+ try {
13
+ await access(fullPath);
14
+ }
15
+ catch {
16
+ missing.push(entry.sourcePath);
17
+ }
18
+ }
19
+ return { checked: uniquePaths.length, missing };
20
+ }
21
+ export function collectEvidenceArtifactPaths(input, artifactRoot) {
22
+ if (!input || typeof input !== "object" || Array.isArray(input))
23
+ return [];
24
+ const bundle = input;
25
+ const paths = [];
26
+ const productRoots = productArtifactRoots(bundle, artifactRoot);
27
+ collectStringValues(bundle.artifacts, paths, artifactRoot);
28
+ const results = Array.isArray(bundle.results) ? bundle.results : [];
29
+ for (const result of results) {
30
+ if (result && typeof result === "object" && !Array.isArray(result)) {
31
+ const resultRecord = result;
32
+ collectStringValues(resultRecord.artifacts, paths, artifactRoot);
33
+ const product = typeof resultRecord.product === "string" ? resultRecord.product : undefined;
34
+ collectEvidencePaths(resultRecord.evidence, paths, product ? (productRoots.get(product) ?? artifactRoot) : artifactRoot);
35
+ }
36
+ }
37
+ collectEvidencePaths(bundle.evidence, paths, artifactRoot, productRoots);
38
+ return dedupeArtifactPathEntries(paths);
39
+ }
40
+ function collectEvidencePaths(input, paths, root, productRoots) {
41
+ const evidence = Array.isArray(input) ? input : [];
42
+ for (const item of evidence) {
43
+ if (item && typeof item === "object" && !Array.isArray(item)) {
44
+ const itemRecord = item;
45
+ const artifactPath = itemRecord.artifactPath;
46
+ if (typeof artifactPath === "string" && artifactPath.length > 0) {
47
+ const source = typeof itemRecord.source === "string" ? itemRecord.source : undefined;
48
+ paths.push({ sourcePath: artifactPath, root: source && productRoots ? (productRoots.get(source) ?? root) : root });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ function productArtifactRoots(bundle, artifactRoot) {
54
+ const roots = new Map();
55
+ const results = Array.isArray(bundle.results) ? bundle.results : [];
56
+ for (const result of results) {
57
+ if (!result || typeof result !== "object" || Array.isArray(result))
58
+ continue;
59
+ const resultRecord = result;
60
+ const product = resultRecord.product;
61
+ const artifacts = resultRecord.artifacts;
62
+ if (typeof product !== "string" || !artifacts || typeof artifacts !== "object" || Array.isArray(artifacts))
63
+ continue;
64
+ const artifactRecord = artifacts;
65
+ const outDir = artifactRecord.outDir;
66
+ const resultPath = artifactRecord.result ?? artifactRecord.results;
67
+ if (typeof outDir === "string" && outDir.length > 0) {
68
+ roots.set(product, isAbsolute(outDir) ? outDir : resolve(artifactRoot, outDir));
69
+ }
70
+ else if (typeof resultPath === "string" && resultPath.length > 0) {
71
+ roots.set(product, dirname(isAbsolute(resultPath) ? resultPath : resolve(artifactRoot, resultPath)));
72
+ }
73
+ }
74
+ return roots;
75
+ }
76
+ function collectStringValues(input, paths, root) {
77
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
78
+ return;
79
+ }
80
+ for (const [name, value] of Object.entries(input)) {
81
+ if (name === "outDir" || name.endsWith(".outDir"))
82
+ continue;
83
+ if (typeof value === "string" && value.length > 0) {
84
+ paths.push({ sourcePath: value, root });
85
+ }
86
+ }
87
+ }
88
+ function dedupeArtifactPathEntries(entries) {
89
+ const seen = new Set();
90
+ const deduped = [];
91
+ for (const entry of entries) {
92
+ const key = `${entry.root}\0${entry.sourcePath}`;
93
+ if (seen.has(key))
94
+ continue;
95
+ seen.add(key);
96
+ deduped.push(entry);
97
+ }
98
+ return deduped;
99
+ }
100
+ export function isRemoteArtifactPath(value) {
101
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
102
+ }
package/dist/badge.js ADDED
@@ -0,0 +1,31 @@
1
+ export function renderAgentCertBadge(bundle) {
2
+ const status = bundle.verdict.passed ? "pass" : "fail";
3
+ const color = bundle.verdict.passed ? "#087f5b" : "#cc2431";
4
+ const value = `${status} ${bundle.verdict.score}`;
5
+ const valueWidth = Math.max(54, value.length * 7 + 18);
6
+ const labelWidth = 66;
7
+ const width = labelWidth + valueWidth;
8
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="20" role="img" aria-label="witnora: ${escapeXml(value)}">
9
+ <title>witnora: ${escapeXml(value)}</title>
10
+ <linearGradient id="s" x2="0" y2="100%">
11
+ <stop offset="0" stop-color="#fff" stop-opacity=".12"/>
12
+ <stop offset="1" stop-color="#000" stop-opacity=".12"/>
13
+ </linearGradient>
14
+ <clipPath id="r"><rect width="${width}" height="20" rx="3" fill="#fff"/></clipPath>
15
+ <g clip-path="url(#r)">
16
+ <rect width="${labelWidth}" height="20" fill="#07172f"/>
17
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color}"/>
18
+ <rect width="${width}" height="20" fill="url(#s)"/>
19
+ </g>
20
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
21
+ <text x="${labelWidth / 2}" y="15" fill="#010101" fill-opacity=".3">witnora</text>
22
+ <text x="${labelWidth / 2}" y="14">witnora</text>
23
+ <text x="${labelWidth + valueWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${escapeXml(value)}</text>
24
+ <text x="${labelWidth + valueWidth / 2}" y="14">${escapeXml(value)}</text>
25
+ </g>
26
+ </svg>
27
+ `;
28
+ }
29
+ function escapeXml(value) {
30
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
31
+ }
@@ -0,0 +1,156 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ const DEFAULT_ADAPTER_PATH = "witnora.browser-adapter.mjs";
5
+ const DEFAULT_REPORT_PATH = ".witnora/browser-adapter/conformance.json";
6
+ export async function runBrowserAdapterCommand(args) {
7
+ const action = args[0] ?? "help";
8
+ if (action === "help" || args.includes("--help") || args.includes("-h")) {
9
+ process.stdout.write(renderBrowserAdapterHelp(action === "help" ? undefined : action));
10
+ return { exitCode: 0 };
11
+ }
12
+ if (action === "init")
13
+ return initializeBrowserAdapter(args.slice(1));
14
+ if (action === "certify")
15
+ return certifyBrowserAdapter(args.slice(1));
16
+ throw new Error(`Unknown browser-adapter command ${JSON.stringify(action)}. Run \`npx witnora browser-adapter --help\`.`);
17
+ }
18
+ export async function initializeBrowserAdapter(args) {
19
+ const requestedPath = flag(args, "--adapter") ?? flag(args, "--out") ?? DEFAULT_ADAPTER_PATH;
20
+ const outPath = resolve(requestedPath);
21
+ await writeStarterFile(outPath, browserAdapterTemplate(), args.includes("--force"));
22
+ process.stdout.write(`Wrote ${outPath}\n\nNext:\n npx witnora browser-adapter certify --adapter ${JSON.stringify(requestedPath)}\n`);
23
+ process.stdout.write("\nThe generated adapter is synthetic and sandbox-only. Replace its callbacks with your customer-owned sandbox boundary before pilot use.\n");
24
+ return { exitCode: 0 };
25
+ }
26
+ export async function certifyBrowserAdapter(args, runtimeOverride, adapterOverride) {
27
+ const runtime = runtimeOverride ?? await loadRuntime();
28
+ const adapterPath = resolve(flag(args, "--adapter") ?? DEFAULT_ADAPTER_PATH);
29
+ const adapterModule = adapterOverride ?? await loadAdapter(adapterPath);
30
+ const kit = adapterModule.browserAdapterKit
31
+ ?? runtime.createCustomerOwnedBrowserAdapterKit(adapterModule.browserAdapterConfig);
32
+ const report = await runtime.runCustomerOwnedBrowserAdapterConformance({
33
+ kit,
34
+ fixture: adapterModule.browserAdapterFixture,
35
+ });
36
+ const reportPath = resolve(flag(args, "--out") ?? DEFAULT_REPORT_PATH);
37
+ await mkdir(dirname(reportPath), { recursive: true });
38
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
39
+ process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 customer-owned browser adapter conformance\n`);
40
+ for (const check of report.checks)
41
+ process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
42
+ process.stdout.write(`Report: ${reportPath}\n`);
43
+ return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
44
+ }
45
+ export function renderBrowserAdapterHelp(action) {
46
+ if (action === "init")
47
+ return `Usage:\n witnora browser-adapter init [--adapter witnora.browser-adapter.mjs] [--force]\n\nWrites a safe, customer-owned browser sandbox adapter template.\n`;
48
+ if (action === "certify")
49
+ return `Usage:\n witnora browser-adapter certify --adapter ./witnora.browser-adapter.mjs [--out .witnora/browser-adapter/conformance.json]\n\nChecks the gateway boundary, allowlists, credential separation, outcome probe, target audit match, revocation, and report redaction.\n`;
50
+ return `Usage:\n witnora browser-adapter init [--adapter witnora.browser-adapter.mjs]\n witnora browser-adapter certify --adapter ./witnora.browser-adapter.mjs\n\nCommands:\n init Write a sandbox-only adapter and deterministic fixture\n certify Run local boundary and evidence conformance\n`;
51
+ }
52
+ async function loadRuntime() {
53
+ try {
54
+ return await import(new URL("./vendor/onegent-runtime/browser-adapter-kit.js", import.meta.url).href);
55
+ }
56
+ catch (error) {
57
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
58
+ if (code === "ERR_MODULE_NOT_FOUND")
59
+ throw new Error("Witnora browser adapter runtime is missing. Reinstall agentcert and retry.");
60
+ throw error;
61
+ }
62
+ }
63
+ async function loadAdapter(adapterPath) {
64
+ let module;
65
+ try {
66
+ module = await import(pathToFileURL(adapterPath).href);
67
+ }
68
+ catch (error) {
69
+ throw new Error(`Could not load browser adapter ${adapterPath}: ${error instanceof Error ? error.message : String(error)}`);
70
+ }
71
+ const hasKit = isRecord(module.browserAdapterKit) && typeof module.browserAdapterKit.prepareExecution === "function";
72
+ if ((!hasKit && !isRecord(module.browserAdapterConfig)) || !isRecord(module.browserAdapterFixture)) {
73
+ throw new Error(`${adapterPath} must export browserAdapterConfig (or a legacy browserAdapterKit) and browserAdapterFixture.`);
74
+ }
75
+ return module;
76
+ }
77
+ function browserAdapterTemplate() {
78
+ return `// Safe starter: local synthetic sandbox state. Never point v0.1 at production.
79
+ const WRITE_SECRET = process.env.BROWSER_SANDBOX_WRITE_KEY ?? "synthetic-write-credential";
80
+ const READ_SECRET = process.env.BROWSER_SANDBOX_READ_KEY ?? "synthetic-read-credential";
81
+ let state = { status: "DRAFT", reference: "ORDER-DEMO-1" };
82
+ const audit = [];
83
+ const parametersDigest = "fixture-parameters-sha256";
84
+
85
+ export const browserAdapterConfig = {
86
+ name: "customer-browser-sandbox",
87
+ targetSystem: "CustomerBrowserSandbox",
88
+ allowedOrigins: ["https://sandbox.example.test"],
89
+ allowedActionTypes: ["SUBMIT"],
90
+ allowedOperation: "order.submit",
91
+ allowedResource: "order:ORDER-DEMO-1",
92
+ sandbox: true,
93
+ resolveWriteCredential() {
94
+ return { reference: "secret-provider://browser/write", secret: WRITE_SECRET, expiresAt: futureExpiry() };
95
+ },
96
+ resolveReadCredential() {
97
+ return { reference: "secret-provider://browser/read", secret: READ_SECRET, expiresAt: futureExpiry() };
98
+ },
99
+ execute({ action, context, credential }) {
100
+ if (credential !== WRITE_SECRET) throw new Error("Write credential boundary failed.");
101
+ const previousState = structuredClone(state);
102
+ state = structuredClone(action.proposedAfterState);
103
+ audit.splice(0, audit.length, {
104
+ targetEventId: "audit-1", actionId: action.id, executionSessionId: context.idempotencyKey,
105
+ occurredAt: new Date().toISOString(), operation: "order.submit", resource: "order:ORDER-DEMO-1",
106
+ parametersDigest, credentialReferenceDigest: "write-reference-digest",
107
+ });
108
+ return { method: "LOCAL_ADAPTER", targetSystem: action.targetSystem, previousState, observedState: structuredClone(state) };
109
+ },
110
+ observe({ credential }) {
111
+ if (credential !== READ_SECRET) throw new Error("Read credential boundary failed.");
112
+ return { observationId: "observation-1", observedAt: new Date().toISOString(), observedState: structuredClone(state), source: "customer-sandbox-read-api" };
113
+ },
114
+ listAuditEvents({ credential }) {
115
+ if (credential !== READ_SECRET) throw new Error("Audit credential boundary failed.");
116
+ return structuredClone(audit);
117
+ },
118
+ revokeWriteCredential() {},
119
+ };
120
+
121
+ export const browserAdapterFixture = {
122
+ action: {
123
+ id: "action-demo-1", idempotencyKey: "adapter-demo-1", workspaceId: "local", workflowId: "browser-adapter-conformance",
124
+ sourceAgentName: "customer-browser-agent", principal: { id: "customer-browser-agent", type: "agent", version: "1.0.0" },
125
+ requestedPermissions: ["order.submit"], actionType: "SUBMIT", targetSystem: "CustomerBrowserSandbox",
126
+ targetUrl: "https://sandbox.example.test/orders/ORDER-DEMO-1", environment: "staging", title: "Submit sandbox order",
127
+ description: "Conformance-only synthetic order submission.", businessObjectType: "order", businessObjectId: "ORDER-DEMO-1",
128
+ beforeState: { status: "DRAFT", reference: "ORDER-DEMO-1" }, proposedAfterState: { status: "SUBMITTED", reference: "ORDER-DEMO-1" },
129
+ fieldsChanged: [{ field: "status", before: "DRAFT", after: "SUBMITTED" }], createdAt: new Date().toISOString(), status: "APPROVED",
130
+ },
131
+ expectedObservedState: { status: "SUBMITTED", reference: "ORDER-DEMO-1" },
132
+ expectedAudit: { operation: "order.submit", resource: "order:ORDER-DEMO-1", parametersDigest },
133
+ forbiddenSecrets: [WRITE_SECRET, READ_SECRET],
134
+ };
135
+
136
+ function futureExpiry() { return new Date(Date.now() + 10 * 60_000).toISOString(); }
137
+ `;
138
+ }
139
+ function flag(args, name) {
140
+ const index = args.indexOf(name);
141
+ return index >= 0 ? args[index + 1] : undefined;
142
+ }
143
+ async function writeStarterFile(path, content, force) {
144
+ await mkdir(dirname(path), { recursive: true });
145
+ try {
146
+ await writeFile(path, content, { flag: force ? "w" : "wx" });
147
+ }
148
+ catch (error) {
149
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST")
150
+ throw new Error(`${path} already exists. Re-run with --force to overwrite it.`);
151
+ throw error;
152
+ }
153
+ }
154
+ function isRecord(value) {
155
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
156
+ }