@ixo/oracle-runtime-workers 0.41.2 → 0.42.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/package.json +2 -2
- package/src/attachments/download.ts +1 -1
- package/src/core/middlewares/dangling-tool-calls.ts +112 -26
- package/src/core/middlewares/result-cap.ts +1 -1
- package/src/core/read-result-tool.ts +70 -7
- package/src/core/runtime-context.ts +2 -2
- package/src/core/subagent-as-tool.ts +49 -2
- package/src/do/ambient.ts +2 -1
- package/src/plugin-api/types.ts +11 -1
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +1 -1
- package/src/plugins/ixo-chain/http.ts +8 -4
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +14 -1
- package/src/plugins/ixo-chain/queries.ts +26 -0
- package/src/plugins/pod-creator/chain-gateway.ts +123 -31
- package/src/plugins/pod-creator/config.ts +81 -1
- package/src/plugins/pod-creator/create-session-store.ts +159 -7
- package/src/plugins/pod-creator/create-tools.ts +403 -28
- package/src/plugins/pod-creator/domain-card.ts +67 -0
- package/src/plugins/pod-creator/index.ts +24 -2
- package/src/plugins/pod-creator/ixo-chain-gateway.ts +901 -0
- package/src/plugins/pod-creator/matrix-media.ts +486 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +5 -2
- package/src/plugins/pod-creator/pod-creator.plugin.ts +30 -9
- package/src/plugins/pod-creator/stage.ts +30 -0
- package/src/plugins/pod-creator/sub-agents.ts +210 -5
|
@@ -9,24 +9,87 @@ import type { ServicePodBlueprint } from './blueprint-types';
|
|
|
9
9
|
*/
|
|
10
10
|
export interface PreparedPodBatch {
|
|
11
11
|
/**
|
|
12
|
-
* The messages, in execution order: `MsgCreateEntity` first, then
|
|
13
|
-
*
|
|
14
|
-
* Each is `{ typeUrl, value }` in proto-JSON (camelCase fields,
|
|
15
|
-
* amounts as strings, bytes as base64
|
|
12
|
+
* The messages, in execution order: `MsgCreateEntity` first, then only
|
|
13
|
+
* `POD_BATCH_TYPE_URLS` (the bundled gateway sends `MsgCreateEntity`
|
|
14
|
+
* alone). Each is `{ typeUrl, value }` in proto-JSON (camelCase fields,
|
|
15
|
+
* integer amounts as strings, bytes as base64, timestamps as RFC 3339
|
|
16
|
+
* strings) with exactly its catalog fields.
|
|
16
17
|
*/
|
|
17
18
|
messages: ITrxMsg[];
|
|
18
19
|
/** Human-readable summary of what the batch creates (at most 1000 characters). */
|
|
19
20
|
summary: string;
|
|
20
21
|
/** Estimated cost, when the gateway can compute it. */
|
|
21
22
|
estimatedCost?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Public documents the messages name (by URL and SHA-256 proof) that are
|
|
25
|
+
* not published yet: `publishDocuments` publishes them once the user
|
|
26
|
+
* approved, before the wallet is asked to sign. Empty when every document
|
|
27
|
+
* the messages name is already public.
|
|
28
|
+
*/
|
|
29
|
+
documents: PendingDocument[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A public document a batch names (the POD's profile, its domain card),
|
|
34
|
+
* reserved on the oracle's homeserver when the batch is prepared and
|
|
35
|
+
* uploaded only after the user approved it.
|
|
36
|
+
*/
|
|
37
|
+
export interface PendingDocument {
|
|
38
|
+
/** The reserved media it is uploaded as: `mxc://<server>/<media id>`. */
|
|
39
|
+
mxc: string;
|
|
40
|
+
/** Its media type, e.g. `application/ld+json`. */
|
|
41
|
+
contentType: string;
|
|
42
|
+
/** The file name the homeserver records. */
|
|
43
|
+
filename: string;
|
|
44
|
+
/** The exact text: the message carries the SHA-256 of these bytes as its proof. */
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* An expected refusal (a missing identity, a homeserver that is down), which
|
|
50
|
+
* the tools report to the model as a result instead of throwing.
|
|
51
|
+
*/
|
|
52
|
+
export interface GatewayRefusal {
|
|
53
|
+
ok: false;
|
|
54
|
+
message: string;
|
|
55
|
+
/** Whether the same call can succeed later unchanged (something was down). */
|
|
56
|
+
retryable: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A gateway answer: the value, or why there is none. */
|
|
60
|
+
export type GatewayResult<T extends object> =
|
|
61
|
+
| ({ ok: true } & T)
|
|
62
|
+
| GatewayRefusal;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a broadcast transaction says about the POD. `pending`: not on chain
|
|
66
|
+
* (or not indexed) within the lookup's time, ask again later with the same
|
|
67
|
+
* hash; `failed`: it failed, or it is not the POD this conversation sent, so
|
|
68
|
+
* asking again cannot help; `unavailable`: the chain could not be read.
|
|
69
|
+
*/
|
|
70
|
+
export type PodConfirmation =
|
|
71
|
+
| { status: 'created'; podDid: string; summary: string }
|
|
72
|
+
| {
|
|
73
|
+
status: 'pending' | 'failed' | 'unavailable';
|
|
74
|
+
message: string;
|
|
75
|
+
retryable: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** What the user's wallet was asked to sign, as the create session records it. */
|
|
79
|
+
export interface SentPodBatch {
|
|
80
|
+
/** The messages exactly as sent (proto-JSON, catalog-validated). */
|
|
81
|
+
messages: ITrxMsg[];
|
|
82
|
+
/** The profile the POD was prepared with (for its domain card). */
|
|
83
|
+
profile: PodProfile;
|
|
22
84
|
}
|
|
23
85
|
|
|
24
86
|
/**
|
|
25
87
|
* The message types a POD creation batch may carry. The chain runs the
|
|
26
88
|
* messages of one transaction in order, all or nothing, so a later message
|
|
27
89
|
* can name what an earlier one creates — the entity DID, its admin account,
|
|
28
|
-
* the collection id —
|
|
29
|
-
*
|
|
90
|
+
* the collection id — only by predicting those values (otherwise the whole
|
|
91
|
+
* transaction fails and nothing is created). The bundled gateway does not
|
|
92
|
+
* predict: it sends `MsgCreateEntity` alone.
|
|
30
93
|
*/
|
|
31
94
|
export const POD_BATCH_TYPE_URLS = [
|
|
32
95
|
'/ixo.entity.v1beta1.MsgCreateEntity',
|
|
@@ -62,49 +125,78 @@ export function podBatchProblem(
|
|
|
62
125
|
return null;
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
/**
|
|
66
|
-
export interface
|
|
67
|
-
|
|
68
|
-
|
|
128
|
+
/** What the user agreed to call the POD, for its on-chain profile. */
|
|
129
|
+
export interface PodProfile {
|
|
130
|
+
/** The POD's name: plain text, 1 to 100 characters. */
|
|
131
|
+
name: string;
|
|
132
|
+
/** What the POD does, in plain text (at most 600 characters). */
|
|
133
|
+
description?: string;
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
/**
|
|
72
|
-
* The chain-facing seam for the create path. The
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* the plugin validates against the `@ixo/ixo-transaction` catalog before
|
|
78
|
-
* storing them, and resolves results.
|
|
137
|
+
* The chain-facing seam for the create path. The oracle never signs
|
|
138
|
+
* creation — the user's wallet does — so a gateway only ever returns
|
|
139
|
+
* proto-JSON messages, which the plugin validates against the
|
|
140
|
+
* `@ixo/ixo-transaction` catalog before storing them, and resolves the
|
|
141
|
+
* broadcast transaction afterwards.
|
|
79
142
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
143
|
+
* The bundled implementation is `createIxoChainGateway()`
|
|
144
|
+
* (`ixo-chain-gateway.ts`): one `MsgCreateEntity` owned by the user, its
|
|
145
|
+
* profile reserved on the oracle's homeserver and published after approval,
|
|
146
|
+
* read back from chain REST; then the POD's domain card. Every method
|
|
147
|
+
* receives the request `RuntimeContext` (the user, the config, the turn's
|
|
148
|
+
* abort signal, the oracle's Matrix device) and answers an expected refusal
|
|
149
|
+
* as a result; a throw is a programming error. The seam is injected so the
|
|
150
|
+
* create path stays unit-testable without a live homeserver or chain, and
|
|
151
|
+
* so a fork can compose a different batch.
|
|
86
152
|
*/
|
|
87
153
|
export interface ChainGateway {
|
|
88
154
|
preparePodBatch(
|
|
89
|
-
input: {
|
|
155
|
+
input: {
|
|
156
|
+
blueprint: ServicePodBlueprint;
|
|
157
|
+
network: Network;
|
|
158
|
+
profile: PodProfile;
|
|
159
|
+
},
|
|
90
160
|
ctx: RuntimeContext,
|
|
91
|
-
): Promise<PreparedPodBatch
|
|
161
|
+
): Promise<GatewayResult<PreparedPodBatch>>;
|
|
162
|
+
/** Publish a prepared batch's pending documents; publishing one twice is a success. */
|
|
163
|
+
publishDocuments(
|
|
164
|
+
input: { documents: readonly PendingDocument[] },
|
|
165
|
+
ctx: RuntimeContext,
|
|
166
|
+
): Promise<GatewayResult<object>>;
|
|
167
|
+
/** Read the transaction back and check it is the POD `sent` describes. */
|
|
92
168
|
confirmPodCreation(
|
|
93
|
-
input: { txHash: string; network: Network },
|
|
169
|
+
input: { txHash: string; network: Network; sent: SentPodBatch },
|
|
170
|
+
ctx: RuntimeContext,
|
|
171
|
+
): Promise<PodConfirmation>;
|
|
172
|
+
/** The batch that adds a confirmed POD's public domain card (one message). */
|
|
173
|
+
prepareDomainCard(
|
|
174
|
+
input: { network: Network; podDid: string; profile: PodProfile },
|
|
175
|
+
ctx: RuntimeContext,
|
|
176
|
+
): Promise<GatewayResult<PreparedPodBatch>>;
|
|
177
|
+
/** Ask the domain indexer to index the POD now; best effort, never throws. */
|
|
178
|
+
requestIndexing(
|
|
179
|
+
input: { network: Network; podDid: string },
|
|
94
180
|
ctx: RuntimeContext,
|
|
95
|
-
): Promise<
|
|
181
|
+
): Promise<void>;
|
|
96
182
|
}
|
|
97
183
|
|
|
98
184
|
const notConfigured = (): never => {
|
|
99
185
|
throw new Error(
|
|
100
|
-
'ChainGateway not configured:
|
|
101
|
-
'
|
|
102
|
-
'transactions.',
|
|
186
|
+
'ChainGateway not configured: this oracle was built without a chain ' +
|
|
187
|
+
'gateway, so it cannot prepare or confirm POD creation transactions.',
|
|
103
188
|
);
|
|
104
189
|
};
|
|
105
190
|
|
|
106
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* A gateway that refuses every call, for a fork that wants the design
|
|
193
|
+
* pipeline without on-chain creation: the create tools report creation as
|
|
194
|
+
* unavailable without calling it.
|
|
195
|
+
*/
|
|
107
196
|
export const notConfiguredChainGateway: ChainGateway = {
|
|
108
197
|
preparePodBatch: async () => notConfigured(),
|
|
198
|
+
publishDocuments: async () => notConfigured(),
|
|
109
199
|
confirmPodCreation: async () => notConfigured(),
|
|
200
|
+
prepareDomainCard: async () => notConfigured(),
|
|
201
|
+
requestIndexing: async () => notConfigured(),
|
|
110
202
|
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@ixo/ixo-transaction';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
8
|
+
import { DOMAIN_INDEXER_URLS } from '../domain-indexer/domain-indexer.plugin';
|
|
8
9
|
import {
|
|
9
10
|
chainIdConfigSchema,
|
|
10
11
|
chainIdsFromConfig,
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export const DEFAULT_NETWORK: Network = 'testnet';
|
|
20
21
|
|
|
22
|
+
/** An IXO entity DID as the chain derives it. */
|
|
23
|
+
const ENTITY_DID = /^did:ixo:entity:[0-9a-f]{32}$/;
|
|
24
|
+
|
|
21
25
|
/**
|
|
22
26
|
* Plugin-owned env vars. The capsules registry URL and `NETWORK` are read as
|
|
23
27
|
* siblings from the merged config (owned by the skills plugin / base schema),
|
|
@@ -42,14 +46,49 @@ export const podCreatorConfigSchema = z.object({
|
|
|
42
46
|
.default(false),
|
|
43
47
|
/** The chain id each network's signing request names. */
|
|
44
48
|
...chainIdConfigSchema.shape,
|
|
49
|
+
/**
|
|
50
|
+
* The relayer node a created POD names, instead of the network's default
|
|
51
|
+
* ({@link POD_RELAYER_NODE_DIDS}).
|
|
52
|
+
*/
|
|
53
|
+
POD_CREATOR_RELAYER_NODE_DID: z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(ENTITY_DID, 'expected an entity DID: did:ixo:entity:<32 hex>')
|
|
56
|
+
.optional(),
|
|
57
|
+
/**
|
|
58
|
+
* The Portal a created POD is linked to, instead of the network's default
|
|
59
|
+
* ({@link PORTAL_URLS}).
|
|
60
|
+
*/
|
|
61
|
+
POD_CREATOR_PORTAL_URL: z.url().optional(),
|
|
45
62
|
});
|
|
46
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The relayer node each network's Portal names on the domains (and PODs) it
|
|
66
|
+
* creates: its `NEXT_PUBLIC_DAO_DID` (ixo-portal `lib/entity/createEntity.ts`),
|
|
67
|
+
* as read from the Portal's recent "Create domain:" transactions. The
|
|
68
|
+
* mainnet Portal lists only entities of this relayer that are verified.
|
|
69
|
+
* Not the oracles' relayer (`lib/oracle/networkConfig.ts`), which only
|
|
70
|
+
* oracle entities name.
|
|
71
|
+
*/
|
|
72
|
+
export const POD_RELAYER_NODE_DIDS: Readonly<Record<Network, string>> = {
|
|
73
|
+
mainnet: 'did:ixo:entity:47d9c64891e77cb8e588d33b83ab3042',
|
|
74
|
+
testnet: 'did:ixo:entity:3d079ebc0b332aad3305bb4a51c72edb',
|
|
75
|
+
devnet: 'did:ixo:entity:ac12c6f8f299071eb24f2a10591d7ab7',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The Portal each network's PODs are opened in. */
|
|
79
|
+
export const PORTAL_URLS: Readonly<Record<Network, string>> = {
|
|
80
|
+
mainnet: 'https://portal.qi.space',
|
|
81
|
+
testnet: 'https://test.portal.qi.space',
|
|
82
|
+
devnet: 'https://dev.portal.qi.space',
|
|
83
|
+
};
|
|
84
|
+
|
|
47
85
|
/** Sibling env + plugin flags the tools read at request time. */
|
|
48
86
|
const configReadSchema = z.object({
|
|
49
87
|
NETWORK: NetworkSchema.optional(),
|
|
50
88
|
POD_CREATOR_ALLOW_MAINNET:
|
|
51
89
|
podCreatorConfigSchema.shape.POD_CREATOR_ALLOW_MAINNET.optional(),
|
|
52
90
|
...chainIdConfigSchema.shape,
|
|
91
|
+
POD_CREATOR_PORTAL_URL: podCreatorConfigSchema.shape.POD_CREATOR_PORTAL_URL,
|
|
53
92
|
});
|
|
54
93
|
|
|
55
94
|
export interface PodCreatorConfig {
|
|
@@ -57,8 +96,12 @@ export interface PodCreatorConfig {
|
|
|
57
96
|
mainnetAllowed: boolean;
|
|
58
97
|
/** The chain id each network's signing request names. */
|
|
59
98
|
chainIds: Readonly<Record<Network, string>>;
|
|
99
|
+
/** The Portal of `network`, without a trailing slash. */
|
|
100
|
+
portalUrl: string;
|
|
60
101
|
}
|
|
61
102
|
|
|
103
|
+
const withoutSlash = (url: string): string => url.replace(/\/+$/, '');
|
|
104
|
+
|
|
62
105
|
/**
|
|
63
106
|
* Network routing + the mainnet opt-in, read from the merged config. Falls
|
|
64
107
|
* back to testnet with mainnet disabled when the config is unreadable — and
|
|
@@ -75,11 +118,48 @@ export function readPodCreatorConfig(ctx: RuntimeContext): PodCreatorConfig {
|
|
|
75
118
|
network: DEFAULT_NETWORK,
|
|
76
119
|
mainnetAllowed: false,
|
|
77
120
|
chainIds: DEFAULT_CHAIN_IDS,
|
|
121
|
+
portalUrl: PORTAL_URLS[DEFAULT_NETWORK],
|
|
78
122
|
};
|
|
79
123
|
}
|
|
124
|
+
const network = parsed.data.NETWORK ?? DEFAULT_NETWORK;
|
|
80
125
|
return {
|
|
81
|
-
network
|
|
126
|
+
network,
|
|
82
127
|
mainnetAllowed: parsed.data.POD_CREATOR_ALLOW_MAINNET ?? false,
|
|
83
128
|
chainIds: chainIdsFromConfig(parsed.data),
|
|
129
|
+
portalUrl: withoutSlash(
|
|
130
|
+
parsed.data.POD_CREATOR_PORTAL_URL ?? PORTAL_URLS[network],
|
|
131
|
+
),
|
|
84
132
|
};
|
|
85
133
|
}
|
|
134
|
+
|
|
135
|
+
const relayerReadSchema = z.object({
|
|
136
|
+
POD_CREATOR_RELAYER_NODE_DID:
|
|
137
|
+
podCreatorConfigSchema.shape.POD_CREATOR_RELAYER_NODE_DID,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/** The relayer node a POD created on `network` names: the operator's, else the network's Portal's. */
|
|
141
|
+
export function relayerNodeDidFor(
|
|
142
|
+
ctx: RuntimeContext,
|
|
143
|
+
network: Network,
|
|
144
|
+
): string {
|
|
145
|
+
const parsed = relayerReadSchema.safeParse(ctx.config);
|
|
146
|
+
return (
|
|
147
|
+
(parsed.success ? parsed.data.POD_CREATOR_RELAYER_NODE_DID : undefined) ??
|
|
148
|
+
POD_RELAYER_NODE_DIDS[network]
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `DOMAIN_INDEXER_URL` is the domain-indexer plugin's; read here as a sibling. */
|
|
153
|
+
const indexerReadSchema = z.object({ DOMAIN_INDEXER_URL: z.url().optional() });
|
|
154
|
+
|
|
155
|
+
/** The domain indexer of `network`, without a trailing slash. */
|
|
156
|
+
export function domainIndexerUrlFor(
|
|
157
|
+
ctx: RuntimeContext,
|
|
158
|
+
network: Network,
|
|
159
|
+
): string {
|
|
160
|
+
const parsed = indexerReadSchema.safeParse(ctx.config);
|
|
161
|
+
return withoutSlash(
|
|
162
|
+
(parsed.success ? parsed.data.DOMAIN_INDEXER_URL : undefined) ??
|
|
163
|
+
DOMAIN_INDEXER_URLS[network],
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ITrxMsgSchema } from '@ixo/ixo-transaction';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import type { RuntimeContext, UserKvSurface } from '../../plugin-api/types';
|
|
4
|
+
import type { SentPodBatch } from './chain-gateway';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Per-(user, thread) state machine for the create path's propose → approve →
|
|
@@ -16,9 +18,13 @@ import type { RuntimeContext, UserKvSurface } from '../../plugin-api/types';
|
|
|
16
18
|
* message after seeing the batch before it can be approved, so a single model
|
|
17
19
|
* turn — steered, say, by injected text in the brief or a section — cannot run
|
|
18
20
|
* prepare → approve → sign on its own.
|
|
21
|
+
*
|
|
22
|
+
* After the commit it keeps what was sent for signing (confirmation checks
|
|
23
|
+
* the transaction against it), the POD confirmed from it, and the one domain
|
|
24
|
+
* card step that may follow.
|
|
19
25
|
*/
|
|
20
26
|
export interface CreateSessionStore {
|
|
21
|
-
/** Record a freshly prepared batch;
|
|
27
|
+
/** Record a freshly prepared batch; anything before it in the thread is superseded. */
|
|
22
28
|
prepared(
|
|
23
29
|
userDid: string,
|
|
24
30
|
threadId: string,
|
|
@@ -37,15 +43,57 @@ export interface CreateSessionStore {
|
|
|
37
43
|
requestId: string,
|
|
38
44
|
): Promise<ApproveOutcome>;
|
|
39
45
|
/**
|
|
40
|
-
* Spend the approval for a sign dispatch
|
|
41
|
-
* is the approved pending batch; the approval is cleared when it
|
|
42
|
-
* a second dispatch needs a fresh approve
|
|
46
|
+
* Spend the approval for a sign dispatch of `sent`. Returns true only when
|
|
47
|
+
* `blobId` is the approved pending batch; the approval is cleared when it
|
|
48
|
+
* matches (a second dispatch needs a fresh approve) and `sent` is recorded
|
|
49
|
+
* as what the wallet was asked to sign.
|
|
43
50
|
*/
|
|
44
|
-
consume(
|
|
45
|
-
|
|
51
|
+
consume(
|
|
52
|
+
userDid: string,
|
|
53
|
+
threadId: string,
|
|
54
|
+
blobId: string,
|
|
55
|
+
sent?: SentPodBatch,
|
|
56
|
+
): Promise<boolean>;
|
|
57
|
+
/** The thread's approval, what its last sign request carried, and the POD confirmed from it. */
|
|
58
|
+
read(
|
|
59
|
+
userDid: string,
|
|
60
|
+
threadId: string,
|
|
61
|
+
): Promise<CreateSessionView | undefined>;
|
|
62
|
+
/** Record the POD confirmed from the sent batch (no-op when nothing was sent). */
|
|
63
|
+
confirmed(
|
|
64
|
+
userDid: string,
|
|
65
|
+
threadId: string,
|
|
66
|
+
pod: { podDid: string; txHash: string },
|
|
67
|
+
): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Claim the confirmed POD's one domain card step. `no-pod` when no POD
|
|
70
|
+
* was confirmed in the thread, `claimed` when the card is being added or
|
|
71
|
+
* was added already.
|
|
72
|
+
*/
|
|
73
|
+
claimCard(userDid: string, threadId: string): Promise<CardClaim>;
|
|
74
|
+
/** End a claimed card step: `added` keeps it closed, `released` lets it run again. */
|
|
75
|
+
settleCard(
|
|
76
|
+
userDid: string,
|
|
77
|
+
threadId: string,
|
|
78
|
+
outcome: 'added' | 'released',
|
|
79
|
+
): Promise<void>;
|
|
80
|
+
/** Drop the session (the design was restarted). */
|
|
46
81
|
clear(userDid: string, threadId: string): Promise<void>;
|
|
47
82
|
}
|
|
48
83
|
|
|
84
|
+
/** The session as the create tools read it. */
|
|
85
|
+
export interface CreateSessionView {
|
|
86
|
+
/** The prepared batch while it holds an approval not spent yet. */
|
|
87
|
+
approvedBlobId?: string;
|
|
88
|
+
sent?: SentPodBatch;
|
|
89
|
+
confirmed?: { podDid: string; txHash: string };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type CardClaim =
|
|
93
|
+
| { status: 'ok'; podDid: string; sent: SentPodBatch }
|
|
94
|
+
| { status: 'no-pod' }
|
|
95
|
+
| { status: 'claimed'; state: 'sending' | 'added' };
|
|
96
|
+
|
|
49
97
|
/** Resolves the create-session store for one request (it lives in the user's database). */
|
|
50
98
|
export type CreateSessionStoreFor = (ctx: RuntimeContext) => CreateSessionStore;
|
|
51
99
|
|
|
@@ -54,11 +102,25 @@ export type ApproveOutcome = 'approved' | 'not-prepared' | 'same-request';
|
|
|
54
102
|
/** `ctx.kv` namespace holding one create session per (user, thread). */
|
|
55
103
|
export const CREATE_SESSION_NAMESPACE = 'pod-creator/create-sessions';
|
|
56
104
|
|
|
105
|
+
const sentSchema = z.object({
|
|
106
|
+
messages: z.array(ITrxMsgSchema),
|
|
107
|
+
profile: z.object({
|
|
108
|
+
name: z.string(),
|
|
109
|
+
description: z.string().optional(),
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
57
113
|
const createSessionSchema = z.object({
|
|
58
114
|
preparedBlobId: z.string(),
|
|
59
115
|
/** The request (turn) that prepared the batch. */
|
|
60
116
|
preparedRequestId: z.string(),
|
|
61
117
|
approved: z.boolean(),
|
|
118
|
+
/** What the last sign request of the prepared batch carried. */
|
|
119
|
+
sent: sentSchema.optional(),
|
|
120
|
+
/** The POD confirmed from `sent`. */
|
|
121
|
+
confirmed: z.object({ podDid: z.string(), txHash: z.string() }).optional(),
|
|
122
|
+
/** The confirmed POD's domain card: being added (claimed), or added. */
|
|
123
|
+
card: z.enum(['sending', 'added']).optional(),
|
|
62
124
|
});
|
|
63
125
|
|
|
64
126
|
type CreateSession = z.infer<typeof createSessionSchema>;
|
|
@@ -156,6 +218,7 @@ export class KvCreateSessionStore implements CreateSessionStore {
|
|
|
156
218
|
userDid: string,
|
|
157
219
|
threadId: string,
|
|
158
220
|
blobId: string,
|
|
221
|
+
sent?: SentPodBatch,
|
|
159
222
|
): Promise<boolean> {
|
|
160
223
|
let spent = false;
|
|
161
224
|
await this.kv.update(
|
|
@@ -167,13 +230,102 @@ export class KvCreateSessionStore implements CreateSessionStore {
|
|
|
167
230
|
session !== undefined &&
|
|
168
231
|
session.preparedBlobId === blobId &&
|
|
169
232
|
session.approved;
|
|
170
|
-
|
|
233
|
+
if (!session || !spent) return session;
|
|
234
|
+
return {
|
|
235
|
+
...session,
|
|
236
|
+
approved: false,
|
|
237
|
+
...(sent !== undefined ? { sent } : {}),
|
|
238
|
+
};
|
|
171
239
|
},
|
|
172
240
|
this.writeOptions,
|
|
173
241
|
);
|
|
174
242
|
return spent;
|
|
175
243
|
}
|
|
176
244
|
|
|
245
|
+
async read(
|
|
246
|
+
userDid: string,
|
|
247
|
+
threadId: string,
|
|
248
|
+
): Promise<CreateSessionView | undefined> {
|
|
249
|
+
const session = parseSession(
|
|
250
|
+
await this.kv.get(CREATE_SESSION_NAMESPACE, key(userDid, threadId)),
|
|
251
|
+
);
|
|
252
|
+
if (!session) return undefined;
|
|
253
|
+
return {
|
|
254
|
+
...(session.approved ? { approvedBlobId: session.preparedBlobId } : {}),
|
|
255
|
+
...(session.sent ? { sent: session.sent } : {}),
|
|
256
|
+
...(session.confirmed ? { confirmed: session.confirmed } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async confirmed(
|
|
261
|
+
userDid: string,
|
|
262
|
+
threadId: string,
|
|
263
|
+
pod: { podDid: string; txHash: string },
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
await this.kv.update(
|
|
266
|
+
CREATE_SESSION_NAMESPACE,
|
|
267
|
+
key(userDid, threadId),
|
|
268
|
+
(current) => {
|
|
269
|
+
const session = parseSession(current);
|
|
270
|
+
if (!session?.sent) return session;
|
|
271
|
+
// The card belongs to the POD: another confirmed POD starts afresh.
|
|
272
|
+
const samePod = session.confirmed?.podDid === pod.podDid;
|
|
273
|
+
const { card: _card, ...rest } = session;
|
|
274
|
+
return {
|
|
275
|
+
...(samePod ? session : rest),
|
|
276
|
+
confirmed: { podDid: pod.podDid, txHash: pod.txHash },
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
this.writeOptions,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async claimCard(userDid: string, threadId: string): Promise<CardClaim> {
|
|
284
|
+
let claim: CardClaim = { status: 'no-pod' };
|
|
285
|
+
await this.kv.update(
|
|
286
|
+
CREATE_SESSION_NAMESPACE,
|
|
287
|
+
key(userDid, threadId),
|
|
288
|
+
(current) => {
|
|
289
|
+
const session = parseSession(current);
|
|
290
|
+
if (!session?.confirmed || !session.sent) {
|
|
291
|
+
claim = { status: 'no-pod' };
|
|
292
|
+
return session;
|
|
293
|
+
}
|
|
294
|
+
if (session.card) {
|
|
295
|
+
claim = { status: 'claimed', state: session.card };
|
|
296
|
+
return session;
|
|
297
|
+
}
|
|
298
|
+
claim = {
|
|
299
|
+
status: 'ok',
|
|
300
|
+
podDid: session.confirmed.podDid,
|
|
301
|
+
sent: session.sent,
|
|
302
|
+
};
|
|
303
|
+
return { ...session, card: 'sending' };
|
|
304
|
+
},
|
|
305
|
+
this.writeOptions,
|
|
306
|
+
);
|
|
307
|
+
return claim;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async settleCard(
|
|
311
|
+
userDid: string,
|
|
312
|
+
threadId: string,
|
|
313
|
+
outcome: 'added' | 'released',
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
await this.kv.update(
|
|
316
|
+
CREATE_SESSION_NAMESPACE,
|
|
317
|
+
key(userDid, threadId),
|
|
318
|
+
(current) => {
|
|
319
|
+
const session = parseSession(current);
|
|
320
|
+
if (!session || session.card !== 'sending') return session;
|
|
321
|
+
if (outcome === 'added') return { ...session, card: 'added' };
|
|
322
|
+
const { card: _card, ...rest } = session;
|
|
323
|
+
return rest;
|
|
324
|
+
},
|
|
325
|
+
this.writeOptions,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
177
329
|
async clear(userDid: string, threadId: string): Promise<void> {
|
|
178
330
|
await this.kv.delete(CREATE_SESSION_NAMESPACE, key(userDid, threadId));
|
|
179
331
|
}
|