Skip to content

Commit de9b27a

Browse files
authored
v0.2.0 — capability/permission vocabulary split + manifest hardening (#3)
* refactor(rename): FeatureManifest -> CapabilityManifest (wave A.1 — manifest type) * refactor(rename): Feature{Status,Record,Store} -> Capability* (wave A.2 — approval) * refactor(rename): FeatureCapabilityRegistry -> PermissionRegistry (wave A.3 — registry) * fix(rename): clean up remaining Feature* leftovers in fixture manifest + fixHint string * refactor(rename): update agent-facing fixHint wording from 'feature' to 'capability' (wave A.5 — vocabulary follow-up) * refactor(wave-b): rename inner-permission field `capabilities[]` -> `permissions[]` and error codes `capability.*` -> `permission.*` - CapabilityManifest.capabilities -> .permissions (the Permission[] array) - ActionManifest.capabilities -> .permissions (the PermissionId[] grant list) - broker fields: capabilityId -> permissionId (NetworkBroker, StorageBroker, ClockBroker, AuditBroker, UiBroker) - AuditTransport.emit envelope: capabilityId -> permissionId - replay envelope: capabilityId -> permissionId - CapabilityError -> PermissionError - error code rename (inner-permission concerns): manifest.capabilities.* -> manifest.permissions.* action.capabilities.* -> action.permissions.* action.capability_ref.* -> action.permission_ref.* capability.{not_object,type,duplicate_id,undeclared,denied,no_broker,action.*} -> permission.* capability.network.{hosts,host_value,methods,method_value,no_transport,bad_url,host_denied,method_denied} -> permission.* capability.storage.{mode,no_transport,read_denied,write_denied} -> permission.* capability.audit.no_transport -> permission.audit.no_transport - fixHints updated to match new vocab - fixture manifest.ts, smoke.ts, end-to-end.ts updated for field + code assertions The outer 'capability' identifier (capabilityId, capabilityVersionHash, AuditEventKind 'capability_emit') is unchanged — that remains the manifest-level identifier per the v0.2 vocab split. Breaking change: manifest hashes change (new shape). Existing approvals will fall back to 'submitted' on first run; require re-approval. Gate: tsc=0, tests=737/737, demo=24/24. * test(hardening): add failing tests for 5 manifest hardening checks (TDD red) * feat(hardening): add manifest hardening checks (5 rules, errors + warnings) - permission.reason.too_short: reason must be >=5 words - action.description.too_short: action description must be >=5 words - permission.network.host_wildcard: no '*' in network hosts - permission.storage.scope_wildcard: no '*' in storage scope - permission.reason.no_action_ref (warning): reason should mention a referencing action id hardenManifest() lives in capability-manifest.ts alongside validateCapabilityManifest(). * feat(approval): wire hardenManifest() into submit(); add SubmitResult.warnings submit() now runs hardening after structural validation. If any hardening errors exist it throws ApprovalError on the first one. Warnings are surfaced on the new SubmitResult shape (CapabilityRecord + warnings[]). * chore(fixture): name referencing actions in each permission.reason Clears the 7 permission.reason.no_action_ref warnings the hardening checks would otherwise surface on the suspicious-transaction-triage demo. Each reason now opens with 'Used by <action.id>' so an approver can trace every grant to its caller. * docs(readme): rewrite for v0.2 — show-by-failing 4-beat opener - Locks the tagline: 'Writmint is a verifier for capabilities an author can't author past.' - 4-beat opener: agent writes manifest with a wildcard host -> submit fails with the verified-real permission.network.host_wildcard error -> fix and resubmit -> run; every brokered call lands in the audit sink. - Updates every API name to v0.2: CapabilityManifest, permissions[], MemoryCapabilityStore, createPermissionRegistry, PermissionError, permission.* error codes, capabilityId (outer wrapper field), scope.cap(). - Mentions hardenManifest() under pillar 1. - Updates test count 737 -> 750. * release: 0.2.0 CHANGELOG entry documents the inner/outer vocabulary split (feature -> capability outer, capabilities[] -> permissions[] inner), manifest hardening (5 rules), and the resulting hash shift requiring re-submit + re-approve for any manifest carried over from v0.1. package.json: version 0.1.0 -> 0.2.0; description rewritten around the locked tagline; keywords swap feature-manifest/capability-scoping for capability-manifest/permission-scoping and add manifest-hardening. ---------
1 parent 607af24 commit de9b27a

16 files changed

Lines changed: 1047 additions & 488 deletions

CHANGELOG.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,102 @@ All notable changes to Writmint will land here. The format follows
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
55
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.2.0] — 2026-05-15
8+
9+
A **breaking** release. The public API splits its vocabulary into an outer
10+
identity layer (the *capability* — the manifest as a unit of governance)
11+
and an inner permission layer (the individual grants the manifest
12+
declares). Adds manifest hardening that runs at submit-time so an agent
13+
gets a structured rejection before approval, not after.
14+
15+
### Breaking changes
16+
17+
#### Outer identity layer renamed: feature → capability
18+
19+
The manifest itself, the lifecycle around it, and the audit envelope all
20+
move to *capability* vocabulary.
21+
22+
| v0.1 | v0.2 |
23+
|---|---|
24+
| `FeatureManifest` | `CapabilityManifest` |
25+
| `MemoryFeatureStore` | `MemoryCapabilityStore` |
26+
| `FeatureStore` interface | `CapabilityStore` |
27+
| `FeatureRecord` | `CapabilityRecord` |
28+
| `FeatureStatus` | `CapabilityStatus` |
29+
| `ApproveInput.featureId` | `ApproveInput.capabilityId` |
30+
| `AuditEvent.featureId` / `featureVersionHash` | `AuditEvent.capabilityId` / `capabilityVersionHash` |
31+
| `AuditEventKind` value `feature_emit` | `capability_emit` (and `capability_call` / `capability_denied`) |
32+
| error code `approval.unknown_feature` | `approval.unknown_capability` |
33+
| `src/feature-manifest.ts` | `src/capability-manifest.ts` |
34+
35+
#### Inner permission layer renamed: capabilities[] → permissions[]
36+
37+
Inside the manifest, what v0.1 called *capabilities* (one per permission
38+
grant) is now *permissions*. The error vocabulary follows.
39+
40+
| v0.1 | v0.2 |
41+
|---|---|
42+
| `CapabilityManifest.capabilities[]` (was on FeatureManifest) | `permissions[]` |
43+
| `ActionManifest.capabilities[]` | `permissions[]` |
44+
| `CapabilityError` class | `PermissionError` |
45+
| broker envelope field `capabilityId` (inner) | `permissionId` |
46+
| `AuditTransport.emit({capabilityId,…})` | `AuditTransport.emit({permissionId,…})` |
47+
| `createFeatureCapabilityRegistry()` | `createPermissionRegistry()` |
48+
| `src/capabilities.ts` | `src/permissions.ts` |
49+
| `capability.*` error codes (`capability.denied`, `capability.network.host_denied`, `capability.storage.write_denied`, `capability.undeclared`, `capability.action.unknown`, `capability.audit.no_transport`, …) | `permission.*` (same suffixes) |
50+
| `manifest.capabilities.type` validator code | `manifest.permissions.type` |
51+
| `action.capability_ref.type` / `.unknown` | `action.permission_ref.type` / `.unknown` |
52+
53+
The outer `capabilityId` on `AuditEvent` is the manifest identity; the
54+
inner `permissionId` (also on `AuditEvent`, and on the broker emit
55+
envelope) is the specific permission entry the event came through. Both
56+
fields ride on every event.
57+
58+
#### Hash shift
59+
60+
`hashManifest()` is unchanged in algorithm, but every shipped manifest
61+
will produce a different `versionHash` under v0.2 because the renamed
62+
object keys (`permissions[]`, etc.) are part of the canonical hash input.
63+
**Re-submit and re-approve any manifest carried over from v0.1.**
64+
65+
### Added
66+
67+
- **`hardenManifest()`** (`src/capability-manifest.ts`) — runs after
68+
structural validation. Enforces five strictness rules that an approver
69+
would otherwise have to check by eye:
70+
- `permission.reason.too_short` — every `reason` must be ≥ 5 words.
71+
- `action.description.too_short` — every action `description` must be
72+
≥ 5 words.
73+
- `permission.network.host_wildcard` — no `*` in any allowed host.
74+
- `permission.storage.scope_wildcard` — no `*` in any storage scope.
75+
- `permission.reason.no_action_ref` (warning) — every permission's
76+
reason should mention at least one action that references it.
77+
- **`ApprovalLifecycle.submit()`** now runs `hardenManifest()` and throws
78+
`ApprovalError` on the first hardening error. The new return shape
79+
`SubmitResult` extends `CapabilityRecord` with a `warnings:
80+
ManifestWarning[]` field carrying non-blocking signals (e.g. the
81+
no-action-ref warning).
82+
- 13 new tests covering each hardening rule (positive + negative) and
83+
`submit()` wiring. Total test count: **737 → 750**.
84+
- README rewritten around a *show-by-failing* opener: the agent writes a
85+
manifest with a wildcard host, `submit()` rejects it with a verified
86+
structured error, the agent fixes and resubmits. Locks the tagline:
87+
*"Writmint is a verifier for capabilities an author can't author past."*
88+
89+
### Changed
90+
91+
- `fixtures/suspicious-transaction-triage/manifest.ts` — every
92+
`permission.reason` now opens with "Used by `<action.id>`…" so the
93+
fixture passes hardening with **0 errors, 0 warnings**.
94+
95+
### Notes
96+
97+
- Pre-stable. Public API surface may change before v1.0.
98+
- Requires Node ≥ 22.
99+
- All 24 demo phases still pass.
100+
101+
[0.2.0]: https://github.com/razukc/writmint/releases/tag/v0.2.0
102+
7103
## [0.1.0] — 2026-05-01
8104

9105
Initial release. Five pillars + the canonical triage demo.

0 commit comments

Comments
 (0)