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.
- package/README.md +275 -12
- package/dist/artifact-manifest.js +37 -0
- package/dist/artifact-validation.js +102 -0
- package/dist/badge.js +31 -0
- package/dist/browser-adapter.js +156 -0
- package/dist/bundle.js +100 -0
- package/dist/cli.js +971 -0
- package/dist/command-help.js +71 -0
- package/dist/companion-artifacts.js +170 -0
- package/dist/conformance.js +115 -0
- package/dist/control-plane.js +409 -0
- package/dist/corpus-governance.js +126 -0
- package/dist/corpus-store.js +239 -0
- package/dist/corpus.js +438 -0
- package/dist/credentials.js +127 -0
- package/dist/evidence-signing.js +85 -0
- package/dist/failure-review.js +188 -0
- package/dist/guided-setup.js +101 -0
- package/dist/index.js +16 -0
- package/dist/lab.js +323 -0
- package/dist/local-server.js +491 -0
- package/dist/monitor.js +100 -0
- package/dist/normalizers.js +265 -0
- package/dist/onboarding-templates.js +158 -0
- package/dist/release-gate.js +457 -0
- package/dist/report.js +165 -0
- package/dist/runner.js +349 -0
- package/dist/sandbox.js +424 -0
- package/dist/schema-validator.js +503 -0
- package/dist/try.js +135 -0
- package/dist/types.js +3 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
- package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
- package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/policies.js +109 -0
- package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
- package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/risk.js +54 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sdk.js +140 -0
- package/dist/vendor/onegent-runtime/service.d.ts +27 -0
- package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/service.js +423 -0
- package/dist/vendor/onegent-runtime/store.d.ts +16 -0
- package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/store.js +29 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-types.js +7 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
- package/dist/vendor/onegent-runtime/types.d.ts +287 -0
- package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/types.js +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
- package/package.json +33 -13
- package/bin/witnora.js +0 -3
package/README.md
CHANGED
|
@@ -1,23 +1,286 @@
|
|
|
1
1
|
# Witnora CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
10
|
-
|
|
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 --
|
|
14
|
-
npx --
|
|
15
|
-
npx
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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
|
+
}
|