@fungi.computer/approvals 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/contract.d.ts +198 -0
- package/dist/contract.js +120 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ledger.d.ts +203 -0
- package/dist/ledger.js +655 -0
- package/docs/api/README.md +1494 -0
- package/package.json +59 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamstack Consulting LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Approvals
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One durable approval record, many renderers. Mycelium owns operation admission;
|
|
4
|
+
this package owns the decision lifecycle beside it. Team storage on the Hub and
|
|
5
|
+
the Oyster host supply the homes. Bonsai is not a ledger home.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @fungi.computer/approvals @fungi.computer/watchdog
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Approvals ships ESM and TypeScript declarations from its root entrypoint. The
|
|
14
|
+
example below uses Node.js 24's native SQLite adapter from Watchdog. Your host
|
|
15
|
+
owns the database, requester identity, current grants and dispatch.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Stage a proposed write without executing it. This disposable in-memory example
|
|
20
|
+
prints `pending`; use a persistent, host-owned database for real requests.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { DatabaseSync } from "node:sqlite";
|
|
24
|
+
import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
|
|
25
|
+
import { ApprovalLedger } from "@fungi.computer/approvals";
|
|
26
|
+
|
|
27
|
+
const database = new DatabaseSync(":memory:");
|
|
28
|
+
try {
|
|
29
|
+
const ledger = new ApprovalLedger({
|
|
30
|
+
owner: fromNodeSqlite(database),
|
|
31
|
+
home: { kind: "team", id: "example-team" },
|
|
32
|
+
now: Date.now,
|
|
33
|
+
});
|
|
34
|
+
const staged = await ledger.request({
|
|
35
|
+
requester: {
|
|
36
|
+
principalId: "agent:example",
|
|
37
|
+
agentId: "example-agent",
|
|
38
|
+
sessionId: "example-session",
|
|
39
|
+
displayName: "Example",
|
|
40
|
+
},
|
|
41
|
+
module: "notes",
|
|
42
|
+
operation: "saveNote",
|
|
43
|
+
access: "write",
|
|
44
|
+
reversibility: "reversible",
|
|
45
|
+
authorityFence: "grant-generation:1",
|
|
46
|
+
idempotencyKey: "turn:1/tool:1/operation:0",
|
|
47
|
+
inputJson: '{"text":"hello"}',
|
|
48
|
+
});
|
|
49
|
+
if (staged.kind !== "record") throw new Error(staged.reason);
|
|
50
|
+
console.log(staged.record.state); // "pending"
|
|
51
|
+
} finally {
|
|
52
|
+
database.close();
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The identities and fence here are illustrative host-attested values, not fields
|
|
57
|
+
to trust from an Agent or browser. Keep the same idempotency key when retrying
|
|
58
|
+
the same operation. `request` stages a record and records the home's
|
|
59
|
+
auto-approve decision without dispatching. `submit` additionally applies a newly
|
|
60
|
+
policy-approved request inline through the authority recheck.
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
The home example stages one pending write using native SQLite. The packed
|
|
65
|
+
consumer in `test/stranger/runtime.mjs` continues through a human answer,
|
|
66
|
+
application and result handoff. It observes one dispatch and one send across
|
|
67
|
+
sequential successful retries; it is not a real Agent transport proof.
|
|
68
|
+
|
|
69
|
+
To integrate those steps, pass `answer` a freshly resolved human member and the
|
|
70
|
+
record's expected revision. Supply `apply` with an `ApprovalApplyHost`:
|
|
71
|
+
`recheck` reads the requester's current authority and fence, and `dispatch`
|
|
72
|
+
calls your existing operation door. Approval does not grant missing authority.
|
|
73
|
+
Finally, `handoff` sends the result through your ordinary message API and
|
|
74
|
+
records its command receipt. Use the notice's stable `deliveryId` for recipient
|
|
75
|
+
deduplication.
|
|
76
|
+
|
|
77
|
+
## Contract model
|
|
78
|
+
|
|
79
|
+
A write needing approval returns a typed pending_approval tool result
|
|
80
|
+
immediately with its recordId and description. It does not run and does not
|
|
81
|
+
suspend the Agent turn. On approval the ledger durably claims, rechecks the
|
|
82
|
+
exact fence and requester's current Mycelium authority, dispatches the existing
|
|
83
|
+
host operation at most once, and stores applied/denied/outcome_unknown. Deferred
|
|
84
|
+
results use the existing ordinary Agent new-message path. Auto-approve applies
|
|
85
|
+
inline through the same record. No MuleTool.resume, Watchdog park/wake, stored
|
|
86
|
+
tool suspension, or unsafe execute replay belongs to this model.
|
|
87
|
+
|
|
88
|
+
The member-bound capability exposes approvals.list/get/answer/setAutoApprove.
|
|
89
|
+
Actors and live grants come from the host, never from request bodies. Reads are
|
|
90
|
+
bounded and project current answer/policy rights. Answering never grants the
|
|
91
|
+
requester authority over an operation.
|
|
92
|
+
|
|
93
|
+
Auto-approve matches a module (or all modules) and access class write/external,
|
|
94
|
+
optionally for one Agent. ON includes irreversible actions, still records the
|
|
95
|
+
decision, and never overrides revoked authority. Local and the solo Oyster owner
|
|
96
|
+
default ON; Teams default OFF. Only the Oyster owner answers on an Oyster.
|
|
97
|
+
Pending approval expires after 24 hours by default; owner/admin configuration
|
|
98
|
+
may change that duration. Expiry never approves.
|
|
99
|
+
|
|
100
|
+
## Current implementation boundary
|
|
101
|
+
|
|
102
|
+
ApprovalLedger uses a host-supplied native SQLite owner. It stages a stable
|
|
103
|
+
intended effect, snapshots metadata before hashing yields, stores private
|
|
104
|
+
input/output separately, serializes answers and claims, and records typed
|
|
105
|
+
completion or unknown outcome without blindly redispatching. A known host
|
|
106
|
+
refusal is distinct from App JSON. recoverClaims transitions orphan claims in
|
|
107
|
+
bounded batches; the native host must call it only at boot, never during a live
|
|
108
|
+
dispatch.
|
|
109
|
+
|
|
110
|
+
ledger.capability({ actor, decision }) binds the four public methods. The host
|
|
111
|
+
must supply fresh member/operation facts on every call and actual durable
|
|
112
|
+
apply/handoff work after a decision; there is no success-shaped default hook.
|
|
113
|
+
List reads are bounded to at most 100 records with a continuation cursor.
|
|
114
|
+
Request bodies cannot select their actor. Policy is persisted by home and
|
|
115
|
+
Agent/module/access family.
|
|
116
|
+
|
|
117
|
+
Deferred, Agent-Session-linked requests also create their result-delivery intent
|
|
118
|
+
in the request transaction. pendingHandoffs returns at most 100 home-local,
|
|
119
|
+
terminal notices (including requests whose deadline has passed). handoff sends
|
|
120
|
+
with one stable recipient-side request identity and stores the real Session
|
|
121
|
+
command receipt. A lost acknowledgement retries that message identity, not the
|
|
122
|
+
effect; inline policy approval creates no extra Agent turn. Private input bytes
|
|
123
|
+
are never notification cargo. The host owns current recipient admission and safe
|
|
124
|
+
ordinary-message presentation of the result.
|
|
125
|
+
|
|
126
|
+
The consumer test uses the public Shiitake memory host with borrowed native
|
|
127
|
+
SQLite owners, loses an acknowledgement after session.send accepts, reopens both
|
|
128
|
+
databases and owners, and observes one new-message turn with the same command
|
|
129
|
+
identity. This is real Session/storage component proof, not a deployed home,
|
|
130
|
+
transport-loss drill or a full-host security law.
|
|
131
|
+
|
|
132
|
+
Host integration is still in progress: the native Team/Oyster compositions,
|
|
133
|
+
ordinary new-message handoff and configurable expiry are not yet wired. The
|
|
134
|
+
current store uses the 24-hour default. Store tests and the page/RPC consumer
|
|
135
|
+
fixture do not establish those production behaviors or the ten full-host laws.
|
|
136
|
+
|
|
137
|
+
## Host admission policy read
|
|
138
|
+
|
|
139
|
+
The runtime calls ledger.selectAutoApprove({ agentId, module, access }) to read
|
|
140
|
+
one effective boolean. This is the same selection request/submit uses: persisted
|
|
141
|
+
Agent rules win over home rules, then a module rule wins over all modules.
|
|
142
|
+
setAutoApprove persists the single enabled field in approval_rules; the read
|
|
143
|
+
neither stages an action nor dispatches it. A true policy value does not attest
|
|
144
|
+
to current grants, the authority fence, or containment.
|
|
145
|
+
|
|
146
|
+
Local is an explicit ledger home with default ON, like Oyster; Team defaults
|
|
147
|
+
OFF. The Local adapter supplies its native SQLite owner. Containment is owned by
|
|
148
|
+
the Local sandbox integration, not this policy read: use a sandbox whenever one
|
|
149
|
+
is available; its absence does not turn auto-approve OFF. An explicit
|
|
150
|
+
requirement for sandboxed execution is a containment choice, not another
|
|
151
|
+
approval switch. This package does not choose a Local database path, start a
|
|
152
|
+
sandbox, display the one-time uncontained notice, or replace the runtime's
|
|
153
|
+
current-authority check.
|
|
154
|
+
|
|
155
|
+
## Capability-attached visual evidence
|
|
156
|
+
|
|
157
|
+
ApprovalRecord optionally carries evidence: an array of at most 4 descriptors,
|
|
158
|
+
each with kind artifact, ref sha256:<64 lowercase hex>, mediaType image/png and
|
|
159
|
+
bytes (integer 1 through 5 MiB). ApprovalEvidenceSchema is the maintained Effect
|
|
160
|
+
Schema decoder. These are defensive display ceilings, not execution authority.
|
|
161
|
+
The ref is a content address, not a URL or a release's opaque artifact: name.
|
|
162
|
+
|
|
163
|
+
Only the submitting capability supplies ApprovalSubmitHost.evidence to
|
|
164
|
+
ledger.submit(request, host). The host-only staging API request(request,
|
|
165
|
+
evidence?) uses the same decoder. ApprovalRequest and tool arguments do not gain
|
|
166
|
+
an evidence field. Never spread Agent/tool input into the host adapter. Metadata
|
|
167
|
+
is snapshotted before hashing yields and persisted with the record; get/list and
|
|
168
|
+
ApprovalView carry it. The immediate pending tool result remains unchanged.
|
|
169
|
+
|
|
170
|
+
Image content is untrusted display-only data, even when capture was performed by
|
|
171
|
+
a capability. Never OCR it into instructions, inject it as Agent text or treat
|
|
172
|
+
it as authority. This package stores metadata only: it does not capture, fetch
|
|
173
|
+
or validate PNG bytes. Producers/store adapters and display renderers own actual
|
|
174
|
+
bounded artifact reads, digest/length verification and maintained image
|
|
175
|
+
decoding. Screenshot capture, workflow result cards, Grove display and the TUI's
|
|
176
|
+
interactive open-screenshot link are separate owner work, not implemented by
|
|
177
|
+
this contract. An idempotent retry retains its original evidence: substituting,
|
|
178
|
+
adding or removing descriptors under the same effect identity returns
|
|
179
|
+
identity-conflict, never silently changes the reviewed image. Omitted and empty
|
|
180
|
+
lists both mean no evidence.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** The effect class is independent of reversibility; ON includes both. */
|
|
3
|
+
export declare const ApprovalAccessSchema: Schema.Literals<readonly ["write", "external"]>;
|
|
4
|
+
/** One operation module, or every module when module is null. */
|
|
5
|
+
export declare const ApprovalFamilySchema: Schema.Struct<{
|
|
6
|
+
readonly module: Schema.NullOr<Schema.String>;
|
|
7
|
+
readonly access: Schema.Literals<readonly ["write", "external"]>;
|
|
8
|
+
}>;
|
|
9
|
+
/** One requesting Agent, or the entire home when agentId is null. */
|
|
10
|
+
export declare const ApprovalScopeSchema: Schema.Struct<{
|
|
11
|
+
readonly agentId: Schema.NullOr<Schema.String>;
|
|
12
|
+
}>;
|
|
13
|
+
/** Stable requester identity supplied by the host, never by tool arguments. */
|
|
14
|
+
export declare const ApprovalRequesterSchema: Schema.Struct<{
|
|
15
|
+
readonly principalId: Schema.String;
|
|
16
|
+
readonly agentId: Schema.NullOr<Schema.String>;
|
|
17
|
+
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
18
|
+
readonly displayName: Schema.String;
|
|
19
|
+
}>;
|
|
20
|
+
/** A renderer sees metadata and a byte digest, never the proposed input. */
|
|
21
|
+
export declare const ApprovalDescriptionSchema: Schema.Struct<{
|
|
22
|
+
readonly title: Schema.String;
|
|
23
|
+
readonly inputBytes: Schema.Int;
|
|
24
|
+
readonly inputSha256: Schema.String;
|
|
25
|
+
}>;
|
|
26
|
+
/** Immediate normal tool result: no effect ran and no Agent turn is suspended. */
|
|
27
|
+
export declare const PendingApprovalSchema: Schema.Struct<{
|
|
28
|
+
readonly kind: Schema.Literal<"pending_approval">;
|
|
29
|
+
readonly recordId: Schema.String;
|
|
30
|
+
readonly description: Schema.Struct<{
|
|
31
|
+
readonly title: Schema.String;
|
|
32
|
+
readonly inputBytes: Schema.Int;
|
|
33
|
+
readonly inputSha256: Schema.String;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
/** Deferred apply is delivered later through the existing Agent new-message path. */
|
|
37
|
+
export type PendingApproval = typeof PendingApprovalSchema.Type;
|
|
38
|
+
/** All renderers use these same durable lifecycle states. */
|
|
39
|
+
export declare const ApprovalStateSchema: Schema.Literals<readonly ["requested", "pending", "approved", "claimed", "applied", "denied", "expired", "outcome_unknown"]>;
|
|
40
|
+
/** Why a decision or apply was refused; unknown dispatch is never a denial. */
|
|
41
|
+
export declare const ApprovalRefusalReasonSchema: Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
|
|
42
|
+
/** Defensive display limits, not authority to fetch or interpret image content. */
|
|
43
|
+
export declare const MAX_APPROVAL_EVIDENCE = 4;
|
|
44
|
+
/** Maximum attested PNG byte length in one evidence descriptor. */
|
|
45
|
+
export declare const MAX_APPROVAL_EVIDENCE_BYTES: number;
|
|
46
|
+
/** Capability-attested content address; image bytes remain untrusted display-only data. */
|
|
47
|
+
export declare const ApprovalEvidenceSchema: Schema.$Array<Schema.Struct<{
|
|
48
|
+
readonly kind: Schema.Literal<"artifact">;
|
|
49
|
+
readonly ref: Schema.String;
|
|
50
|
+
readonly mediaType: Schema.Literal<"image/png">;
|
|
51
|
+
readonly bytes: Schema.Int;
|
|
52
|
+
}>>;
|
|
53
|
+
/** Metadata only: never raw image bytes, prose, URLs or executable content. */
|
|
54
|
+
export type ApprovalEvidence = typeof ApprovalEvidenceSchema.Type;
|
|
55
|
+
/** Public record. Effect input, output and executable continuations stay host-private. */
|
|
56
|
+
export declare const ApprovalRecordSchema: Schema.Struct<{
|
|
57
|
+
readonly recordId: Schema.String;
|
|
58
|
+
readonly requester: Schema.Struct<{
|
|
59
|
+
readonly principalId: Schema.String;
|
|
60
|
+
readonly agentId: Schema.NullOr<Schema.String>;
|
|
61
|
+
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
62
|
+
readonly displayName: Schema.String;
|
|
63
|
+
}>;
|
|
64
|
+
readonly module: Schema.String;
|
|
65
|
+
readonly operation: Schema.String;
|
|
66
|
+
readonly access: Schema.Literals<readonly ["write", "external"]>;
|
|
67
|
+
readonly description: Schema.Struct<{
|
|
68
|
+
readonly title: Schema.String;
|
|
69
|
+
readonly inputBytes: Schema.Int;
|
|
70
|
+
readonly inputSha256: Schema.String;
|
|
71
|
+
}>;
|
|
72
|
+
readonly evidence: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
73
|
+
readonly kind: Schema.Literal<"artifact">;
|
|
74
|
+
readonly ref: Schema.String;
|
|
75
|
+
readonly mediaType: Schema.Literal<"image/png">;
|
|
76
|
+
readonly bytes: Schema.Int;
|
|
77
|
+
}>>>;
|
|
78
|
+
readonly authorityFence: Schema.String;
|
|
79
|
+
readonly reversibility: Schema.Literals<readonly ["reversible", "irreversible-external"]>;
|
|
80
|
+
readonly state: Schema.Literals<readonly ["requested", "pending", "approved", "claimed", "applied", "denied", "expired", "outcome_unknown"]>;
|
|
81
|
+
readonly by: Schema.NullOr<Schema.Union<readonly [Schema.Struct<{
|
|
82
|
+
readonly kind: Schema.Literal<"human">;
|
|
83
|
+
readonly memberId: Schema.String;
|
|
84
|
+
}>, Schema.Struct<{
|
|
85
|
+
readonly kind: Schema.Literal<"policy">;
|
|
86
|
+
}>]>>;
|
|
87
|
+
readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
88
|
+
readonly requestedAt: Schema.Int;
|
|
89
|
+
readonly expiresAt: Schema.Int;
|
|
90
|
+
readonly updatedAt: Schema.Int;
|
|
91
|
+
readonly revision: Schema.Int;
|
|
92
|
+
}>;
|
|
93
|
+
/** Rights are recomputed for the holder on each read, not persisted on the record. */
|
|
94
|
+
export declare const ApprovalViewSchema: Schema.Struct<{
|
|
95
|
+
readonly record: Schema.Struct<{
|
|
96
|
+
readonly recordId: Schema.String;
|
|
97
|
+
readonly requester: Schema.Struct<{
|
|
98
|
+
readonly principalId: Schema.String;
|
|
99
|
+
readonly agentId: Schema.NullOr<Schema.String>;
|
|
100
|
+
readonly sessionId: Schema.NullOr<Schema.String>;
|
|
101
|
+
readonly displayName: Schema.String;
|
|
102
|
+
}>;
|
|
103
|
+
readonly module: Schema.String;
|
|
104
|
+
readonly operation: Schema.String;
|
|
105
|
+
readonly access: Schema.Literals<readonly ["write", "external"]>;
|
|
106
|
+
readonly description: Schema.Struct<{
|
|
107
|
+
readonly title: Schema.String;
|
|
108
|
+
readonly inputBytes: Schema.Int;
|
|
109
|
+
readonly inputSha256: Schema.String;
|
|
110
|
+
}>;
|
|
111
|
+
readonly evidence: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
112
|
+
readonly kind: Schema.Literal<"artifact">;
|
|
113
|
+
readonly ref: Schema.String;
|
|
114
|
+
readonly mediaType: Schema.Literal<"image/png">;
|
|
115
|
+
readonly bytes: Schema.Int;
|
|
116
|
+
}>>>;
|
|
117
|
+
readonly authorityFence: Schema.String;
|
|
118
|
+
readonly reversibility: Schema.Literals<readonly ["reversible", "irreversible-external"]>;
|
|
119
|
+
readonly state: Schema.Literals<readonly ["requested", "pending", "approved", "claimed", "applied", "denied", "expired", "outcome_unknown"]>;
|
|
120
|
+
readonly by: Schema.NullOr<Schema.Union<readonly [Schema.Struct<{
|
|
121
|
+
readonly kind: Schema.Literal<"human">;
|
|
122
|
+
readonly memberId: Schema.String;
|
|
123
|
+
}>, Schema.Struct<{
|
|
124
|
+
readonly kind: Schema.Literal<"policy">;
|
|
125
|
+
}>]>>;
|
|
126
|
+
readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
127
|
+
readonly requestedAt: Schema.Int;
|
|
128
|
+
readonly expiresAt: Schema.Int;
|
|
129
|
+
readonly updatedAt: Schema.Int;
|
|
130
|
+
readonly revision: Schema.Int;
|
|
131
|
+
}>;
|
|
132
|
+
readonly canAnswer: Schema.Boolean;
|
|
133
|
+
readonly canSetAutoApprove: Schema.Boolean;
|
|
134
|
+
}>;
|
|
135
|
+
/** Bounded reads with an opaque storage cursor. */
|
|
136
|
+
export declare const ApprovalListInputSchema: Schema.Struct<{
|
|
137
|
+
readonly state: Schema.optionalKey<Schema.Literals<readonly ["requested", "pending", "approved", "claimed", "applied", "denied", "expired", "outcome_unknown"]>>;
|
|
138
|
+
readonly cursor: Schema.optionalKey<Schema.String>;
|
|
139
|
+
readonly limit: Schema.optionalKey<Schema.Int>;
|
|
140
|
+
}>;
|
|
141
|
+
/** Locate one public view without carrying actor identity in request data. */
|
|
142
|
+
export declare const ApprovalGetInputSchema: Schema.Struct<{
|
|
143
|
+
readonly recordId: Schema.String;
|
|
144
|
+
}>;
|
|
145
|
+
/** An answer is a decision, never a confirm-labelled write itself. */
|
|
146
|
+
export declare const ApprovalAnswerInputSchema: Schema.Struct<{
|
|
147
|
+
readonly recordId: Schema.String;
|
|
148
|
+
readonly decision: Schema.Literals<readonly ["approve", "deny"]>;
|
|
149
|
+
readonly expectedRevision: Schema.Int;
|
|
150
|
+
}>;
|
|
151
|
+
/** Explicit policy mutation. A false rule narrows a broader true rule. */
|
|
152
|
+
export declare const AutoApproveInputSchema: Schema.Struct<{
|
|
153
|
+
readonly scope: Schema.Struct<{
|
|
154
|
+
readonly agentId: Schema.NullOr<Schema.String>;
|
|
155
|
+
}>;
|
|
156
|
+
readonly family: Schema.Struct<{
|
|
157
|
+
readonly module: Schema.NullOr<Schema.String>;
|
|
158
|
+
readonly access: Schema.Literals<readonly ["write", "external"]>;
|
|
159
|
+
}>;
|
|
160
|
+
readonly enabled: Schema.Boolean;
|
|
161
|
+
}>;
|
|
162
|
+
/** Durable approval visible to every renderer. */
|
|
163
|
+
export type ApprovalRecord = typeof ApprovalRecordSchema.Type;
|
|
164
|
+
/** Current holder's projection. A disabled button is not an authorization check. */
|
|
165
|
+
export type ApprovalView = typeof ApprovalViewSchema.Type;
|
|
166
|
+
/** Bounded listing request. */
|
|
167
|
+
export type ApprovalListInput = typeof ApprovalListInputSchema.Type;
|
|
168
|
+
/** Revision-pinned answer. No actor identity is accepted on the wire. */
|
|
169
|
+
export type ApprovalAnswerInput = typeof ApprovalAnswerInputSchema.Type;
|
|
170
|
+
/** One per-home or per-Agent module/access rule. */
|
|
171
|
+
export type AutoApproveInput = typeof AutoApproveInputSchema.Type;
|
|
172
|
+
/** One typed refusal shared by answer/apply/policy entrypoints. */
|
|
173
|
+
export type ApprovalRefusal = Readonly<{
|
|
174
|
+
kind: "refused";
|
|
175
|
+
reason: typeof ApprovalRefusalReasonSchema.Type;
|
|
176
|
+
}>;
|
|
177
|
+
/** The member-bound capability. Host authentication supplies the actor. */
|
|
178
|
+
export type Approvals = Readonly<{
|
|
179
|
+
/** List a bounded page of this home's records for the current member. */
|
|
180
|
+
list(input: ApprovalListInput): Promise<Readonly<{
|
|
181
|
+
items: readonly ApprovalView[];
|
|
182
|
+
nextCursor: string | null;
|
|
183
|
+
}>>;
|
|
184
|
+
/** Read a member-scoped record view, or null when inaccessible. */
|
|
185
|
+
get(input: Readonly<{
|
|
186
|
+
recordId: string;
|
|
187
|
+
}>): Promise<ApprovalView | null>;
|
|
188
|
+
/** Record one current-member decision against the supplied record revision. */
|
|
189
|
+
answer(input: ApprovalAnswerInput): Promise<Readonly<{
|
|
190
|
+
kind: "answered";
|
|
191
|
+
view: ApprovalView;
|
|
192
|
+
}> | ApprovalRefusal>;
|
|
193
|
+
/** Update family policy without granting operation authority. */
|
|
194
|
+
setAutoApprove(input: AutoApproveInput): Promise<Readonly<{
|
|
195
|
+
kind: "updated";
|
|
196
|
+
rule: AutoApproveInput;
|
|
197
|
+
}> | ApprovalRefusal>;
|
|
198
|
+
}>;
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const Identity = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
3
|
+
const Revision = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
|
|
4
|
+
const Timestamp = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
|
|
5
|
+
/** The effect class is independent of reversibility; ON includes both. */
|
|
6
|
+
export const ApprovalAccessSchema = Schema.Literals(["write", "external"]);
|
|
7
|
+
/** One operation module, or every module when module is null. */
|
|
8
|
+
export const ApprovalFamilySchema = Schema.Struct({
|
|
9
|
+
module: Schema.NullOr(Identity),
|
|
10
|
+
access: ApprovalAccessSchema,
|
|
11
|
+
});
|
|
12
|
+
/** One requesting Agent, or the entire home when agentId is null. */
|
|
13
|
+
export const ApprovalScopeSchema = Schema.Struct({
|
|
14
|
+
agentId: Schema.NullOr(Identity),
|
|
15
|
+
});
|
|
16
|
+
/** Stable requester identity supplied by the host, never by tool arguments. */
|
|
17
|
+
export const ApprovalRequesterSchema = Schema.Struct({
|
|
18
|
+
principalId: Identity,
|
|
19
|
+
agentId: Schema.NullOr(Identity),
|
|
20
|
+
sessionId: Schema.NullOr(Identity),
|
|
21
|
+
displayName: Identity,
|
|
22
|
+
});
|
|
23
|
+
/** A renderer sees metadata and a byte digest, never the proposed input. */
|
|
24
|
+
export const ApprovalDescriptionSchema = Schema.Struct({
|
|
25
|
+
title: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(512)),
|
|
26
|
+
inputBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
27
|
+
inputSha256: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
|
|
28
|
+
});
|
|
29
|
+
/** Immediate normal tool result: no effect ran and no Agent turn is suspended. */
|
|
30
|
+
export const PendingApprovalSchema = Schema.Struct({
|
|
31
|
+
kind: Schema.Literal("pending_approval"),
|
|
32
|
+
recordId: Identity,
|
|
33
|
+
description: ApprovalDescriptionSchema,
|
|
34
|
+
});
|
|
35
|
+
/** All renderers use these same durable lifecycle states. */
|
|
36
|
+
export const ApprovalStateSchema = Schema.Literals([
|
|
37
|
+
"requested",
|
|
38
|
+
"pending",
|
|
39
|
+
"approved",
|
|
40
|
+
"claimed",
|
|
41
|
+
"applied",
|
|
42
|
+
"denied",
|
|
43
|
+
"expired",
|
|
44
|
+
"outcome_unknown",
|
|
45
|
+
]);
|
|
46
|
+
/** Why a decision or apply was refused; unknown dispatch is never a denial. */
|
|
47
|
+
export const ApprovalRefusalReasonSchema = Schema.Literals([
|
|
48
|
+
"not-found",
|
|
49
|
+
"not-eligible",
|
|
50
|
+
"already-answered",
|
|
51
|
+
"stale-revision",
|
|
52
|
+
"expired",
|
|
53
|
+
"declined",
|
|
54
|
+
"fenced",
|
|
55
|
+
"not-granted",
|
|
56
|
+
"read-only",
|
|
57
|
+
"pending",
|
|
58
|
+
"in-flight",
|
|
59
|
+
"outcome-unknown",
|
|
60
|
+
"identity-conflict",
|
|
61
|
+
]);
|
|
62
|
+
/** Defensive display limits, not authority to fetch or interpret image content. */
|
|
63
|
+
export const MAX_APPROVAL_EVIDENCE = 4;
|
|
64
|
+
/** Maximum attested PNG byte length in one evidence descriptor. */
|
|
65
|
+
export const MAX_APPROVAL_EVIDENCE_BYTES = 5 * 1024 * 1024;
|
|
66
|
+
/** Capability-attested content address; image bytes remain untrusted display-only data. */
|
|
67
|
+
export const ApprovalEvidenceSchema = Schema.Array(Schema.Struct({
|
|
68
|
+
kind: Schema.Literal("artifact"),
|
|
69
|
+
ref: Schema.String.check(Schema.isPattern(/^sha256:[a-f0-9]{64}$/)),
|
|
70
|
+
mediaType: Schema.Literal("image/png"),
|
|
71
|
+
bytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(MAX_APPROVAL_EVIDENCE_BYTES)),
|
|
72
|
+
})).check(Schema.isMaxLength(MAX_APPROVAL_EVIDENCE));
|
|
73
|
+
/** Public record. Effect input, output and executable continuations stay host-private. */
|
|
74
|
+
export const ApprovalRecordSchema = Schema.Struct({
|
|
75
|
+
recordId: Identity,
|
|
76
|
+
requester: ApprovalRequesterSchema,
|
|
77
|
+
module: Identity,
|
|
78
|
+
operation: Identity,
|
|
79
|
+
access: ApprovalAccessSchema,
|
|
80
|
+
description: ApprovalDescriptionSchema,
|
|
81
|
+
evidence: Schema.optionalKey(ApprovalEvidenceSchema),
|
|
82
|
+
authorityFence: Identity,
|
|
83
|
+
reversibility: Schema.Literals(["reversible", "irreversible-external"]),
|
|
84
|
+
state: ApprovalStateSchema,
|
|
85
|
+
by: Schema.NullOr(Schema.Union([
|
|
86
|
+
Schema.Struct({ kind: Schema.Literal("human"), memberId: Identity }),
|
|
87
|
+
Schema.Struct({ kind: Schema.Literal("policy") }),
|
|
88
|
+
])),
|
|
89
|
+
reason: Schema.NullOr(ApprovalRefusalReasonSchema),
|
|
90
|
+
requestedAt: Timestamp,
|
|
91
|
+
expiresAt: Timestamp,
|
|
92
|
+
updatedAt: Timestamp,
|
|
93
|
+
revision: Revision,
|
|
94
|
+
});
|
|
95
|
+
/** Rights are recomputed for the holder on each read, not persisted on the record. */
|
|
96
|
+
export const ApprovalViewSchema = Schema.Struct({
|
|
97
|
+
record: ApprovalRecordSchema,
|
|
98
|
+
canAnswer: Schema.Boolean,
|
|
99
|
+
canSetAutoApprove: Schema.Boolean,
|
|
100
|
+
});
|
|
101
|
+
/** Bounded reads with an opaque storage cursor. */
|
|
102
|
+
export const ApprovalListInputSchema = Schema.Struct({
|
|
103
|
+
state: Schema.optionalKey(ApprovalStateSchema),
|
|
104
|
+
cursor: Schema.optionalKey(Identity),
|
|
105
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(100))),
|
|
106
|
+
});
|
|
107
|
+
/** Locate one public view without carrying actor identity in request data. */
|
|
108
|
+
export const ApprovalGetInputSchema = Schema.Struct({ recordId: Identity });
|
|
109
|
+
/** An answer is a decision, never a confirm-labelled write itself. */
|
|
110
|
+
export const ApprovalAnswerInputSchema = Schema.Struct({
|
|
111
|
+
recordId: Identity,
|
|
112
|
+
decision: Schema.Literals(["approve", "deny"]),
|
|
113
|
+
expectedRevision: Revision,
|
|
114
|
+
});
|
|
115
|
+
/** Explicit policy mutation. A false rule narrows a broader true rule. */
|
|
116
|
+
export const AutoApproveInputSchema = Schema.Struct({
|
|
117
|
+
scope: ApprovalScopeSchema,
|
|
118
|
+
family: ApprovalFamilySchema,
|
|
119
|
+
enabled: Schema.Boolean,
|
|
120
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export { ApprovalLedger, ApprovalRefusalSchema, DEFAULT_APPROVAL_EXPIRY_MS, type AutoApproveQuery, type ApprovalHome, type ApprovalRequest, type ApprovalAnswerer, type ApprovalAdmission, type ApprovalRuleSetter, type ApprovalApplyHost, type ApprovalSubmitHost, type ApprovalDispatchCompletion, type ApprovalApplyResult, type ApprovalResultNotice, type ApprovalHandoffResult, } from "./ledger.js";
|
package/dist/index.js
ADDED