@ixo/oracle-runtime-workers 0.13.0 → 0.15.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 +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { BatchMessageRoute, ITrxMsg, Network } from '@ixo/ixo-transaction';
|
|
2
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
3
|
+
import type { ServicePodBlueprint } from './blueprint-types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The POD creation batch the user's wallet will sign as ONE transaction:
|
|
7
|
+
* proto-JSON messages the Portal can show and re-validate against the
|
|
8
|
+
* `@ixo/ixo-transaction` catalog — never encoded bytes.
|
|
9
|
+
*/
|
|
10
|
+
export interface PreparedPodBatch {
|
|
11
|
+
/**
|
|
12
|
+
* The messages, in execution order: `MsgCreateEntity` first, then the
|
|
13
|
+
* claim collection(s), entity accounts and grants (`POD_BATCH_TYPE_URLS`).
|
|
14
|
+
* Each is `{ typeUrl, value }` in proto-JSON (camelCase fields, integer
|
|
15
|
+
* amounts as strings, bytes as base64) with exactly its catalog fields.
|
|
16
|
+
*/
|
|
17
|
+
messages: ITrxMsg[];
|
|
18
|
+
/** Human-readable summary of what the batch creates (at most 1000 characters). */
|
|
19
|
+
summary: string;
|
|
20
|
+
/** Estimated cost, when the gateway can compute it. */
|
|
21
|
+
estimatedCost?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The message types a POD creation batch may carry. The chain runs the
|
|
26
|
+
* messages of one transaction in order, all or nothing, so a later message
|
|
27
|
+
* can name what an earlier one creates — the entity DID, its admin account,
|
|
28
|
+
* the collection id — as long as the gateway predicts those values right
|
|
29
|
+
* (otherwise the whole transaction fails and nothing is created).
|
|
30
|
+
*/
|
|
31
|
+
export const POD_BATCH_TYPE_URLS = [
|
|
32
|
+
'/ixo.entity.v1beta1.MsgCreateEntity',
|
|
33
|
+
// A collection's payment accounts must be accounts of the entity.
|
|
34
|
+
'/ixo.entity.v1beta1.MsgCreateEntityAccount',
|
|
35
|
+
'/ixo.claims.v1beta1.MsgCreateCollection',
|
|
36
|
+
// Submit / evaluate authorizations from the collection admin account.
|
|
37
|
+
'/ixo.claims.v1beta1.MsgCreateClaimAuthorization',
|
|
38
|
+
// Generic or bank-send grants from an entity account.
|
|
39
|
+
'/ixo.entity.v1beta1.MsgGrantEntityAccountAuthz',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
const CREATE_ENTITY = POD_BATCH_TYPE_URLS[0];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Why a validated batch is not a POD creation batch, or null when it is:
|
|
46
|
+
* exactly one `MsgCreateEntity`, first, and only `POD_BATCH_TYPE_URLS`.
|
|
47
|
+
*/
|
|
48
|
+
export function podBatchProblem(
|
|
49
|
+
routes: readonly BatchMessageRoute[],
|
|
50
|
+
): string | null {
|
|
51
|
+
const allowed: readonly string[] = POD_BATCH_TYPE_URLS;
|
|
52
|
+
const foreign = routes.filter((route) => !allowed.includes(route.typeUrl));
|
|
53
|
+
if (foreign.length > 0) {
|
|
54
|
+
return `A POD creation batch carries only ${POD_BATCH_TYPE_URLS.join(', ')}; it had ${foreign
|
|
55
|
+
.map((route) => route.typeUrl)
|
|
56
|
+
.join(', ')}`;
|
|
57
|
+
}
|
|
58
|
+
const entities = routes.filter((route) => route.typeUrl === CREATE_ENTITY);
|
|
59
|
+
if (entities.length !== 1 || routes[0]?.typeUrl !== CREATE_ENTITY) {
|
|
60
|
+
return `A POD creation batch creates exactly one entity, with ${CREATE_ENTITY} as its first message`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The created POD, resolved from the broadcast transaction. */
|
|
66
|
+
export interface CreatedPod {
|
|
67
|
+
podDid: string;
|
|
68
|
+
summary: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The chain-facing seam for the create path. The planned concrete implementation
|
|
73
|
+
* calls the IXO MCP server (hosted on Cloudflare) to compose the POD-creation
|
|
74
|
+
* batch (`MsgCreateEntity` + the claim collection + authz grants) and to read a
|
|
75
|
+
* broadcast transaction back. The oracle never signs creation — the user's
|
|
76
|
+
* wallet does — so the gateway only ever returns proto-JSON messages, which
|
|
77
|
+
* the plugin validates against the `@ixo/ixo-transaction` catalog before
|
|
78
|
+
* storing them, and resolves results.
|
|
79
|
+
*
|
|
80
|
+
* Both methods receive the request `RuntimeContext` because reaching the IXO MCP
|
|
81
|
+
* server follows the runtime's remote-MCP pattern (see the sandbox plugin):
|
|
82
|
+
* resolve the server DID via did:web and mint a per-user `ixo:*` UCAN invocation
|
|
83
|
+
* through `ctx.ucan` for the `Authorization` header, with the MCP URL read from
|
|
84
|
+
* `ctx.config`. The seam is injected so the create path stays unit-testable
|
|
85
|
+
* without a live server or chain.
|
|
86
|
+
*/
|
|
87
|
+
export interface ChainGateway {
|
|
88
|
+
preparePodBatch(
|
|
89
|
+
input: { blueprint: ServicePodBlueprint; network: Network },
|
|
90
|
+
ctx: RuntimeContext,
|
|
91
|
+
): Promise<PreparedPodBatch>;
|
|
92
|
+
confirmPodCreation(
|
|
93
|
+
input: { txHash: string; network: Network },
|
|
94
|
+
ctx: RuntimeContext,
|
|
95
|
+
): Promise<CreatedPod>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const notConfigured = (): never => {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'ChainGateway not configured: the POD create path needs a chain gateway ' +
|
|
101
|
+
'wired to the IXO MCP server before it can prepare or confirm ' +
|
|
102
|
+
'transactions.',
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Default gateway that errors until a real one is wired. */
|
|
107
|
+
export const notConfiguredChainGateway: ChainGateway = {
|
|
108
|
+
preparePodBatch: async () => notConfigured(),
|
|
109
|
+
confirmPodCreation: async () => notConfigured(),
|
|
110
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CHAIN_IDS,
|
|
3
|
+
NetworkSchema,
|
|
4
|
+
type Network,
|
|
5
|
+
} from '@ixo/ixo-transaction';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
8
|
+
import {
|
|
9
|
+
chainIdConfigSchema,
|
|
10
|
+
chainIdsFromConfig,
|
|
11
|
+
} from '../ixo-transaction/chain-ids';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The network every pod-creator surface falls back to when the merged config
|
|
15
|
+
* carries no readable `NETWORK` — the safe one. On a Workers oracle the base
|
|
16
|
+
* env schema always fills `NETWORK` (default `mainnet`), so in practice the
|
|
17
|
+
* mainnet opt-in below is what keeps an unconfigured oracle off mainnet.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_NETWORK: Network = 'testnet';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Plugin-owned env vars. The capsules registry URL and `NETWORK` are read as
|
|
23
|
+
* siblings from the merged config (owned by the skills plugin / base schema),
|
|
24
|
+
* so they are intentionally NOT redeclared here — two plugins declaring one
|
|
25
|
+
* key with different schemas would collide in the config-schema registry.
|
|
26
|
+
* The chain ids are the ixo-transaction plugin's own schema objects, which
|
|
27
|
+
* both plugins declare (one set of variables for every wallet-signing
|
|
28
|
+
* plugin; the composition treats the shared objects as one key).
|
|
29
|
+
*/
|
|
30
|
+
export const podCreatorConfigSchema = z.object({
|
|
31
|
+
/**
|
|
32
|
+
* Must be explicitly enabled before a mainnet creation batch can be
|
|
33
|
+
* prepared. Testnet / devnet require no opt-in. Accepts a real boolean
|
|
34
|
+
* (tests / programmatic config) or the string `'true'` / `'false'` (a
|
|
35
|
+
* Worker var).
|
|
36
|
+
*/
|
|
37
|
+
POD_CREATOR_ALLOW_MAINNET: z
|
|
38
|
+
.union([
|
|
39
|
+
z.boolean(),
|
|
40
|
+
z.enum(['true', 'false']).transform((value) => value === 'true'),
|
|
41
|
+
])
|
|
42
|
+
.default(false),
|
|
43
|
+
/** The chain id each network's signing request names. */
|
|
44
|
+
...chainIdConfigSchema.shape,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Sibling env + plugin flags the tools read at request time. */
|
|
48
|
+
const configReadSchema = z.object({
|
|
49
|
+
NETWORK: NetworkSchema.optional(),
|
|
50
|
+
POD_CREATOR_ALLOW_MAINNET:
|
|
51
|
+
podCreatorConfigSchema.shape.POD_CREATOR_ALLOW_MAINNET.optional(),
|
|
52
|
+
...chainIdConfigSchema.shape,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export interface PodCreatorConfig {
|
|
56
|
+
network: Network;
|
|
57
|
+
mainnetAllowed: boolean;
|
|
58
|
+
/** The chain id each network's signing request names. */
|
|
59
|
+
chainIds: Readonly<Record<Network, string>>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Network routing + the mainnet opt-in, read from the merged config. Falls
|
|
64
|
+
* back to testnet with mainnet disabled when the config is unreadable — and
|
|
65
|
+
* says so, since a silent network fallback on the create path would mask
|
|
66
|
+
* misconfiguration.
|
|
67
|
+
*/
|
|
68
|
+
export function readPodCreatorConfig(ctx: RuntimeContext): PodCreatorConfig {
|
|
69
|
+
const parsed = configReadSchema.safeParse(ctx.config);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
ctx.logger.warn(
|
|
72
|
+
'[pod-creator] config unreadable; defaulting to testnet with mainnet disabled',
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
network: DEFAULT_NETWORK,
|
|
76
|
+
mainnetAllowed: false,
|
|
77
|
+
chainIds: DEFAULT_CHAIN_IDS,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
network: parsed.data.NETWORK ?? DEFAULT_NETWORK,
|
|
82
|
+
mainnetAllowed: parsed.data.POD_CREATOR_ALLOW_MAINNET ?? false,
|
|
83
|
+
chainIds: chainIdsFromConfig(parsed.data),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { RuntimeContext, UserKvSurface } from '../../plugin-api/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-(user, thread) state machine for the create path's propose → approve →
|
|
6
|
+
* commit handoff. Exactly one batch can be pending per key: `prepared`
|
|
7
|
+
* supersedes anything before it, `approve` binds the user's go-ahead to that
|
|
8
|
+
* exact blobId, and `consume` spends the approval — so every wallet sign
|
|
9
|
+
* dispatch requires a fresh, explicit approval and a sign request can never be
|
|
10
|
+
* replayed from a stale one. Keyed by user DID as well as thread so a batch
|
|
11
|
+
* prepared in another thread (or by another user in a shared thread) can never
|
|
12
|
+
* be approved here.
|
|
13
|
+
*
|
|
14
|
+
* The session also records the request (turn) that prepared the batch, and
|
|
15
|
+
* approval is refused within that same request: the user has to send a new
|
|
16
|
+
* message after seeing the batch before it can be approved, so a single model
|
|
17
|
+
* turn — steered, say, by injected text in the brief or a section — cannot run
|
|
18
|
+
* prepare → approve → sign on its own.
|
|
19
|
+
*/
|
|
20
|
+
export interface CreateSessionStore {
|
|
21
|
+
/** Record a freshly prepared batch; any prior approval is superseded. */
|
|
22
|
+
prepared(
|
|
23
|
+
userDid: string,
|
|
24
|
+
threadId: string,
|
|
25
|
+
blobId: string,
|
|
26
|
+
requestId: string,
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Approve the pending batch from request `requestId`. `not-prepared` when
|
|
30
|
+
* `blobId` is not the batch prepared for this user+thread; `same-request`
|
|
31
|
+
* when the approval comes from the request that prepared it.
|
|
32
|
+
*/
|
|
33
|
+
approve(
|
|
34
|
+
userDid: string,
|
|
35
|
+
threadId: string,
|
|
36
|
+
blobId: string,
|
|
37
|
+
requestId: string,
|
|
38
|
+
): Promise<ApproveOutcome>;
|
|
39
|
+
/**
|
|
40
|
+
* Spend the approval for a sign dispatch. Returns true only when `blobId`
|
|
41
|
+
* is the approved pending batch; the approval is cleared when it matches, so
|
|
42
|
+
* a second dispatch needs a fresh approve.
|
|
43
|
+
*/
|
|
44
|
+
consume(userDid: string, threadId: string, blobId: string): Promise<boolean>;
|
|
45
|
+
/** Drop the session (after on-chain confirmation). */
|
|
46
|
+
clear(userDid: string, threadId: string): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolves the create-session store for one request (it lives in the user's database). */
|
|
50
|
+
export type CreateSessionStoreFor = (ctx: RuntimeContext) => CreateSessionStore;
|
|
51
|
+
|
|
52
|
+
export type ApproveOutcome = 'approved' | 'not-prepared' | 'same-request';
|
|
53
|
+
|
|
54
|
+
/** `ctx.kv` namespace holding one create session per (user, thread). */
|
|
55
|
+
export const CREATE_SESSION_NAMESPACE = 'pod-creator/create-sessions';
|
|
56
|
+
|
|
57
|
+
const createSessionSchema = z.object({
|
|
58
|
+
preparedBlobId: z.string(),
|
|
59
|
+
/** The request (turn) that prepared the batch. */
|
|
60
|
+
preparedRequestId: z.string(),
|
|
61
|
+
approved: z.boolean(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
type CreateSession = z.infer<typeof createSessionSchema>;
|
|
65
|
+
|
|
66
|
+
/** Mirrors the blob store's default TTL — the approval is useless without the blob. */
|
|
67
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
68
|
+
const DEFAULT_MAX_ENTRIES = 1000;
|
|
69
|
+
|
|
70
|
+
export interface KvCreateSessionStoreOptions {
|
|
71
|
+
maxEntries?: number;
|
|
72
|
+
ttlMs?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A JSON pair cannot collide the way a joined string could. */
|
|
76
|
+
const key = (userDid: string, threadId: string): string =>
|
|
77
|
+
JSON.stringify([userDid, threadId]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A stored session that no longer parses is treated as no session: the only
|
|
81
|
+
* consequence is that the user has to prepare the batch again, which is the
|
|
82
|
+
* safe direction for an approval gate.
|
|
83
|
+
*/
|
|
84
|
+
function parseSession(value: unknown): CreateSession | undefined {
|
|
85
|
+
const parsed = createSessionSchema.safeParse(value);
|
|
86
|
+
return parsed.success ? parsed.data : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* {@link CreateSessionStore} over the host's `ctx.kv` rows in the user's own
|
|
91
|
+
* database, so a prepared or approved batch survives the user object being
|
|
92
|
+
* evicted between the user's "yes" and the sign request. Each transition is
|
|
93
|
+
* one atomic read-modify-write.
|
|
94
|
+
*/
|
|
95
|
+
export class KvCreateSessionStore implements CreateSessionStore {
|
|
96
|
+
private readonly writeOptions: { idleTtlMs: number; maxEntries: number };
|
|
97
|
+
|
|
98
|
+
constructor(
|
|
99
|
+
private readonly kv: UserKvSurface,
|
|
100
|
+
options: KvCreateSessionStoreOptions = {},
|
|
101
|
+
) {
|
|
102
|
+
this.writeOptions = {
|
|
103
|
+
idleTtlMs: options.ttlMs ?? DEFAULT_TTL_MS,
|
|
104
|
+
maxEntries: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async prepared(
|
|
109
|
+
userDid: string,
|
|
110
|
+
threadId: string,
|
|
111
|
+
blobId: string,
|
|
112
|
+
requestId: string,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const session: CreateSession = {
|
|
115
|
+
preparedBlobId: blobId,
|
|
116
|
+
preparedRequestId: requestId,
|
|
117
|
+
approved: false,
|
|
118
|
+
};
|
|
119
|
+
await this.kv.set(
|
|
120
|
+
CREATE_SESSION_NAMESPACE,
|
|
121
|
+
key(userDid, threadId),
|
|
122
|
+
session,
|
|
123
|
+
this.writeOptions,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async approve(
|
|
128
|
+
userDid: string,
|
|
129
|
+
threadId: string,
|
|
130
|
+
blobId: string,
|
|
131
|
+
requestId: string,
|
|
132
|
+
): Promise<ApproveOutcome> {
|
|
133
|
+
let outcome: ApproveOutcome = 'not-prepared';
|
|
134
|
+
await this.kv.update(
|
|
135
|
+
CREATE_SESSION_NAMESPACE,
|
|
136
|
+
key(userDid, threadId),
|
|
137
|
+
(current) => {
|
|
138
|
+
const session = parseSession(current);
|
|
139
|
+
if (!session || session.preparedBlobId !== blobId) {
|
|
140
|
+
outcome = 'not-prepared';
|
|
141
|
+
return session;
|
|
142
|
+
}
|
|
143
|
+
if (session.preparedRequestId === requestId) {
|
|
144
|
+
outcome = 'same-request';
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
outcome = 'approved';
|
|
148
|
+
return { ...session, approved: true };
|
|
149
|
+
},
|
|
150
|
+
this.writeOptions,
|
|
151
|
+
);
|
|
152
|
+
return outcome;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async consume(
|
|
156
|
+
userDid: string,
|
|
157
|
+
threadId: string,
|
|
158
|
+
blobId: string,
|
|
159
|
+
): Promise<boolean> {
|
|
160
|
+
let spent = false;
|
|
161
|
+
await this.kv.update(
|
|
162
|
+
CREATE_SESSION_NAMESPACE,
|
|
163
|
+
key(userDid, threadId),
|
|
164
|
+
(current) => {
|
|
165
|
+
const session = parseSession(current);
|
|
166
|
+
spent =
|
|
167
|
+
session !== undefined &&
|
|
168
|
+
session.preparedBlobId === blobId &&
|
|
169
|
+
session.approved;
|
|
170
|
+
return session && spent ? { ...session, approved: false } : session;
|
|
171
|
+
},
|
|
172
|
+
this.writeOptions,
|
|
173
|
+
);
|
|
174
|
+
return spent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async clear(userDid: string, threadId: string): Promise<void> {
|
|
178
|
+
await this.kv.delete(CREATE_SESSION_NAMESPACE, key(userDid, threadId));
|
|
179
|
+
}
|
|
180
|
+
}
|