@fungi.computer/veil 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,75 @@ Workerd's `startTls({ expectedServerHostname })` verifies the certificate
84
84
  against that name; it reports a verification failure on the first read, which
85
85
  the door types as `unavailable`.
86
86
 
87
+ ## Platform snapshot egress (private, not enabled by this change)
88
+
89
+ `configuredProviderEgress(env)` (from `./gatekeeper`) also owns the platform
90
+ snapshot routes below. Like the existing Hub search caller
91
+ (`apps/hub/src/agent-web-search.ts`, `configuredWebSearch`), a caller obtains
92
+ `baseUrl(route)` and passes credential-free requests through
93
+ `providerFetch(egress, route)`. Veil alone reads the bindings, pins the
94
+ destination and attaches the credential. The new holdings callers are composed
95
+ later; these routes neither mount Hub endpoints nor schedule jobs.
96
+
97
+ | Route | Destination / allowed calls | Private bindings |
98
+ | --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
99
+ | `solana-rpc` | Helius mainnet, exactly `https://mainnet.helius-rpc.com/`, POST | `SOLANA_RPC_URL` (that credential-free root), `SOLANA_RPC_API_KEY` (Worker secret) |
100
+ | `solana-rpc-fallback` | One configured QuickNode Solana mainnet endpoint, exactly its HTTPS root, POST | `SOLANA_RPC_FALLBACK_URL` (`https://<endpoint>.solana-mainnet.quiknode.pro/`), `SOLANA_RPC_FALLBACK_API_KEY` (Worker secret) |
101
+ | `sanctions-screen` | GET exactly `https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML` | `SANCTIONS_SDN_URL` (that exact URL, opt-in) |
102
+
103
+ The RPC routes accept only single JSON-RPC 2.0 calls to `getAccountInfo`,
104
+ `getMultipleAccounts`, `getProgramAccounts`, `getTokenSupply` and `getSlot`:
105
+ mint/program-owner boot checks, token and lock account reads, supply and
106
+ finalized-slot reconciliation. Transactions, simulation, airdrops, arbitrary
107
+ methods and batches are refused. The holdings owner still owns finality, params,
108
+ maintained account decoders and reconciliation. QuickNode is the proposal's
109
+ second-provider choice, not an automatic retry: its caller must apply the same
110
+ snapshot checks on fallback.
111
+
112
+ Helius receives `?api-key=<secret>`; QuickNode receives `/<secret>/`. Neither
113
+ credential-bearing URL is returned from `baseUrl`. Caller headers, queries and
114
+ alternate paths are not forwarded. Keep keys in the existing private
115
+ Worker-secret custody, never Team storage, public config, telemetry or logs. The
116
+ final transport sees the authorized URL and must not log it. Missing/invalid
117
+ bindings refuse `unavailable` before fetch; other destinations or methods refuse
118
+ `denied`. RPC redirects and non-2xx responses refuse `unavailable`; no upstream
119
+ error body, headers or URL is returned. Successful bodies are bounded to 64 MiB
120
+ and refused on a credential echo (raw, JSON-escaped or percent-encoded); RPC
121
+ request bodies are bounded to 64 KiB. Transport failures, including aborts,
122
+ expose only fixed `VeilError` codes. The existing streaming model/search routes
123
+ and model-key rules are unchanged.
124
+
125
+ The sanctions GET alone may manually follow **one HTTP 302** from the exact OFAC
126
+ export to
127
+ `https://wc2h-sls-prod-public-published.s3.us-gov-west-1.amazonaws.com`, with a
128
+ path beginning `/Published/` and ending `/SDN.XML`. OFAC's signed query is
129
+ retained; caller headers/credentials and upstream cookies are not forwarded. The
130
+ Location string is capped at 8,192 code units, must have no userinfo or
131
+ fragment, and cannot change HTTPS or port 443. Veil cancels the redirect body
132
+ before the second GET, uses the original abort signal (including any caller
133
+ deadline, never restarted) and keeps the same 64 MiB download bound. Another
134
+ host/path, a missing/invalid Location, a second redirect or any other redirect
135
+ status refuses `unavailable`. Direct entry at S3 and non-GET sanctions requests
136
+ remain refused.
137
+
138
+ **Sanctions choice:** use the official OFAC SDN XML list, not the Chainalysis
139
+ free API. Read-only confirmation on 2026-10-31 found actual
140
+ `Digital Currency Address - SOL` identifiers in the official export (including
141
+ ID 194579), establishing Solana-address coverage without assuming a vendor's
142
+ chain support. OFAC's public list requires **no credential**, so there is no
143
+ `SANCTIONS_API_KEY` binding or invented anonymous-key fallback. F5 must parse
144
+ the XML through a maintained decoder, match the SOL identifiers, and fail closed
145
+ on an unavailable or unusable list; this door does not claim to screen persons,
146
+ countries or unlisted addresses.
147
+
148
+ Provider contract sources:
149
+ [Helius RPC](https://www.helius.dev/docs/rpc/overview),
150
+ [QuickNode endpoints](https://www.quicknode.com/docs/solana/endpoints),
151
+ [OFAC data formats](https://ofac.treasury.gov/specially-designated-nationals-list-data-formats-data-schemas)
152
+ and the exact official export above. Unit fixtures prove custody and route
153
+ policy, not provisioned keys, paid-plan large-snapshot support, live RPC
154
+ reconciliation or live screening; F4/F5 own those opt-in proofs.
155
+
87
156
  ## Model credentials and failure handling
88
157
 
89
158
  `src/model-credentials.ts` selects which key class may serve an inference
@@ -0,0 +1,141 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import type { PublicEgressPolicy } from "../public-egress-policy.js";
4
+ export type ConnectionsRefusal = "invalid_input" | "denied" | "revoked" | "conflict" | "unsupported" | "too_large" | "reconnect_required" | "invalid_response" | "unavailable";
5
+ /** Fixed codes only: upstream bodies, URLs and credential bytes never enter errors. */
6
+ export declare class ConnectionsError extends Error {
7
+ readonly code: ConnectionsRefusal;
8
+ readonly name = "ConnectionsError";
9
+ constructor(code: ConnectionsRefusal, options?: ErrorOptions);
10
+ }
11
+ export declare const identifier: Schema.String;
12
+ declare const placementSchema: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
13
+ readonly header: Schema.String;
14
+ }>, Schema.Struct<{
15
+ readonly query: Schema.String;
16
+ }>]>;
17
+ export type Placement = typeof placementSchema.Type;
18
+ /** How the final request is authorized. OAuth joins this union in slice 3. */
19
+ export declare const credentialSchema: Schema.Union<readonly [Schema.Struct<{
20
+ readonly kind: Schema.Literal<"none">;
21
+ }>, Schema.Struct<{
22
+ readonly kind: Schema.Literal<"static">;
23
+ readonly placement: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
24
+ readonly header: Schema.String;
25
+ }>, Schema.Struct<{
26
+ readonly query: Schema.String;
27
+ }>]>;
28
+ }>]>;
29
+ export type Credential = typeof credentialSchema.Type;
30
+ export type SurfaceView = Readonly<{
31
+ kind: "mcp";
32
+ url: string;
33
+ }> | Readonly<{
34
+ kind: "openapi";
35
+ baseUrl: string;
36
+ table: unknown;
37
+ }>;
38
+ export declare const toolsSchema: Schema.Union<readonly [Schema.Literal<"all">, Schema.$Array<Schema.String>]>;
39
+ export type Tools = typeof toolsSchema.Type;
40
+ export declare const draftSchema: Schema.Struct<{
41
+ /** Idempotency key: a replay with the same request returns the same connection. */
42
+ readonly requestId: Schema.String;
43
+ readonly name: Schema.String;
44
+ readonly surface: Schema.Union<readonly [Schema.Struct<{
45
+ readonly kind: Schema.Literal<"mcp">;
46
+ readonly url: Schema.String;
47
+ }>, Schema.Struct<{
48
+ readonly kind: Schema.Literal<"openapi">;
49
+ readonly baseUrl: Schema.String;
50
+ readonly table: Schema.Unknown;
51
+ }>]>;
52
+ readonly credential: Schema.Union<readonly [Schema.Struct<{
53
+ readonly kind: Schema.Literal<"none">;
54
+ }>, Schema.Struct<{
55
+ readonly kind: Schema.Literal<"static">;
56
+ readonly placement: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
57
+ readonly header: Schema.String;
58
+ }>, Schema.Struct<{
59
+ readonly query: Schema.String;
60
+ }>]>;
61
+ }>]>;
62
+ readonly secret: Schema.optional<Schema.String>;
63
+ }>;
64
+ export type ConnectionDraft = typeof draftSchema.Type;
65
+ export declare const grantInputSchema: Schema.Struct<{
66
+ readonly connectionId: Schema.String;
67
+ readonly agentId: Schema.String;
68
+ readonly tools: Schema.Union<readonly [Schema.Literal<"all">, Schema.$Array<Schema.String>]>;
69
+ }>;
70
+ export type GrantInput = typeof grantInputSchema.Type;
71
+ export declare const egressRefSchema: Schema.Struct<{
72
+ readonly connectionId: Schema.String;
73
+ readonly grantVersion: Schema.Number;
74
+ }>;
75
+ export type EgressRef = typeof egressRefSchema.Type;
76
+ /** Where custody finds one connection; carries no secret. */
77
+ export type CustodyAddress = Readonly<{
78
+ ownerId: string;
79
+ connectionId: string;
80
+ credential: Credential;
81
+ }>;
82
+ /**
83
+ * Owner-side credential custody. `sealed` stores ciphertext, `nango` a locator.
84
+ * Only `inject` ever opens a credential, and it sends the request itself.
85
+ */
86
+ export type CustodyAdapter = Readonly<{
87
+ put: (address: CustodyAddress, secret: string) => Effect.Effect<string, ConnectionsError>;
88
+ inject: (address: CustodyAddress, locator: string, request: Request) => Effect.Effect<Response, ConnectionsError>;
89
+ remove: (address: CustodyAddress, locator: string) => Effect.Effect<"deleted" | "absent", ConnectionsError>;
90
+ }>;
91
+ export type SqlValue = string | number | null;
92
+ /** The owner's SQLite in Cloudflare `ctx.storage` shape. */
93
+ export type SqlStore = Readonly<{
94
+ sql: Readonly<{
95
+ exec: (statement: string, ...bindings: SqlValue[]) => Readonly<{
96
+ toArray: () => readonly unknown[];
97
+ }>;
98
+ }>;
99
+ transactionSync: <A>(operation: () => A) => A;
100
+ }>;
101
+ export type ConnectionsOptions = Readonly<{
102
+ ownerId: string;
103
+ store: SqlStore;
104
+ custody: CustodyAdapter;
105
+ /** Host authority read: may this actor manage connections, or still stand as a grantor? */
106
+ authorize: (actorId: string, purpose: "manage" | "grantor") => boolean;
107
+ egressPolicy?: PublicEgressPolicy;
108
+ clock: () => number;
109
+ ids: () => string;
110
+ }>;
111
+ /** Secret-free, versioned, and safe to hand to an Agent. */
112
+ export type ConnectionProjection = Readonly<{
113
+ revision: number;
114
+ connections: readonly Readonly<{
115
+ connectionId: string;
116
+ name: string;
117
+ surface: SurfaceView;
118
+ tools: Tools;
119
+ authorityVersion: number;
120
+ grantVersion: number;
121
+ }>[];
122
+ }>;
123
+ export type ConnectionView = Readonly<{
124
+ connectionId: string;
125
+ name: string;
126
+ surface: Readonly<{
127
+ kind: "mcp" | "openapi";
128
+ origin: string;
129
+ }>;
130
+ credentialKind: Credential["kind"];
131
+ state: "active" | "reconnect_required" | "revoked";
132
+ authorityVersion: number;
133
+ grants: readonly Readonly<{
134
+ agentId: string;
135
+ tools: Tools;
136
+ version: number;
137
+ }>[];
138
+ }>;
139
+ /** The Agent's narrowed capability: only the owner opens the credential. */
140
+ export type NarrowFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
141
+ export {};
@@ -0,0 +1,55 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ /** Fixed codes only: upstream bodies, URLs and credential bytes never enter errors. */
4
+ export class ConnectionsError extends Error {
5
+ code;
6
+ name = "ConnectionsError";
7
+ constructor(code, options) {
8
+ super(code, options);
9
+ this.code = code;
10
+ }
11
+ }
12
+ export const identifier = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9_.:-]{1,128}$/u)));
13
+ const headerName = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9-]{1,64}$/u)));
14
+ const queryName = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9_.-]{1,64}$/u)));
15
+ const version = Schema.Number.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1)));
16
+ const placementSchema = Schema.Union([
17
+ Schema.Literal("bearer"),
18
+ Schema.Struct({ header: headerName }),
19
+ Schema.Struct({ query: queryName }),
20
+ ]);
21
+ /** How the final request is authorized. OAuth joins this union in slice 3. */
22
+ export const credentialSchema = Schema.Union([
23
+ Schema.Struct({ kind: Schema.Literal("none") }),
24
+ Schema.Struct({ kind: Schema.Literal("static"), placement: placementSchema }),
25
+ ]);
26
+ /** What the Agent can call. The OpenAPI table is decoded by Forage before it is stored. */
27
+ const surfaceDraftSchema = Schema.Union([
28
+ Schema.Struct({ kind: Schema.Literal("mcp"), url: Schema.String }),
29
+ Schema.Struct({
30
+ kind: Schema.Literal("openapi"),
31
+ baseUrl: Schema.String,
32
+ table: Schema.Unknown,
33
+ }),
34
+ ]);
35
+ export const toolsSchema = Schema.Union([
36
+ Schema.Literal("all"),
37
+ Schema.Array(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256)))).pipe(Schema.check(Schema.isMaxLength(256))),
38
+ ]);
39
+ export const draftSchema = Schema.Struct({
40
+ /** Idempotency key: a replay with the same request returns the same connection. */
41
+ requestId: identifier,
42
+ name: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128))),
43
+ surface: surfaceDraftSchema,
44
+ credential: credentialSchema,
45
+ secret: Schema.optional(Schema.String.pipe(Schema.check(Schema.isPattern(/^[\x21-\x7e]{1,16384}$/u)))),
46
+ });
47
+ export const grantInputSchema = Schema.Struct({
48
+ connectionId: identifier,
49
+ agentId: identifier,
50
+ tools: toolsSchema,
51
+ });
52
+ export const egressRefSchema = Schema.Struct({
53
+ connectionId: identifier,
54
+ grantVersion: version,
55
+ });
@@ -0,0 +1,48 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ConnectionsError, type ConnectionDraft, type GrantInput, type ConnectionsOptions, type EgressRef, type NarrowFetch, type SurfaceView } from "./contract.js";
3
+ /** One owner's connections: attempts, connections, grants, receipts and cleanup in its SQLite. */
4
+ export declare function createConnections(options: ConnectionsOptions): {
5
+ begin: (actor: string, input: ConnectionDraft) => Effect.Effect<{
6
+ connectionId: string;
7
+ authorizeUrl: null;
8
+ }, ConnectionsError, never>;
9
+ grant: (actor: string, input: GrantInput) => Effect.Effect<{
10
+ grantVersion: number;
11
+ }, ConnectionsError, never>;
12
+ ungrant: (actor: string, input: {
13
+ connectionId: string;
14
+ agentId: string;
15
+ }) => Effect.Effect<void, ConnectionsError, never>;
16
+ revoke: (actor: string, input: {
17
+ connectionId: string;
18
+ }) => Effect.Effect<void, ConnectionsError, never>;
19
+ projection: (agentId: string) => Effect.Effect<{
20
+ revision: number;
21
+ connections: {
22
+ connectionId: string;
23
+ name: string;
24
+ surface: SurfaceView;
25
+ tools: readonly string[] | "all";
26
+ authorityVersion: number;
27
+ grantVersion: number;
28
+ }[];
29
+ }, ConnectionsError, never>;
30
+ list: (actor: string) => Effect.Effect<Readonly<{
31
+ connectionId: string;
32
+ name: string;
33
+ surface: Readonly<{
34
+ kind: "mcp" | "openapi";
35
+ origin: string;
36
+ }>;
37
+ credentialKind: import("./contract.js").Credential["kind"];
38
+ state: "active" | "reconnect_required" | "revoked";
39
+ authorityVersion: number;
40
+ grants: readonly Readonly<{
41
+ agentId: string;
42
+ tools: import("./contract.js").Tools;
43
+ version: number;
44
+ }>[];
45
+ }>[], ConnectionsError, never>;
46
+ egress: (agentId: string, input: EgressRef) => NarrowFetch;
47
+ sweep: () => Effect.Effect<void, ConnectionsError, never>;
48
+ };