@ixo/oracle-runtime-workers 0.22.0 → 0.24.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 +15 -13
- package/package.json +10 -4
- package/src/action-proposals/policy.ts +54 -0
- package/src/action-proposals/reconcile.ts +87 -0
- package/src/action-proposals/store.ts +863 -0
- package/src/action-proposals/topic-budget.ts +84 -0
- package/src/action-proposals/topic-chain.ts +272 -0
- package/src/action-proposals/topic-effects.ts +44 -0
- package/src/action-proposals/topic-errors.ts +79 -0
- package/src/action-proposals/topic-history.ts +267 -0
- package/src/action-proposals/topic-matrix.ts +292 -0
- package/src/action-proposals/topic-prepare.ts +299 -0
- package/src/action-proposals/topic-store.ts +999 -0
- package/src/action-proposals/topic-verify.ts +279 -0
- package/src/action-proposals/verify.ts +223 -0
- package/src/channels/auth.ts +6 -1
- package/src/channels/contract.ts +4 -0
- package/src/channels/turns.ts +1 -0
- package/src/core/env.ts +93 -6
- package/src/core/main-agent-types.ts +8 -0
- package/src/core/main-agent.ts +2 -0
- package/src/core/runtime-context.ts +12 -0
- package/src/delivery/plan.ts +43 -2
- package/src/delivery/schema.ts +25 -1
- package/src/delivery/types.ts +13 -2
- package/src/do/ambient.ts +6 -0
- package/src/do/contracts.ts +39 -0
- package/src/do/user-oracle-do.ts +1275 -8
- package/src/hub-actions/client.ts +163 -0
- package/src/hub-actions/request.ts +110 -0
- package/src/hub-actions/store.ts +485 -0
- package/src/matrix/gateway-do.ts +176 -0
- package/src/matrix/reply-parts.ts +8 -2
- package/src/memory/session-history-indexer.ts +30 -3
- package/src/memory/user-context-fetcher.ts +36 -12
- package/src/plugin-api/types.ts +54 -0
- package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
- package/src/plugins/action-proposals/evidence.ts +67 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/memory/index.ts +4 -0
- package/src/plugins/memory/memory-entity.ts +49 -0
- package/src/plugins/memory/memory-tools.ts +7 -4
- package/src/plugins/memory/memory-ucan.ts +10 -1
- package/src/plugins/memory/memory.plugin.ts +22 -7
- package/src/shell/app.ts +195 -1
package/src/shell/app.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionProposalWireSchema,
|
|
3
|
+
TopicReceiptReferencesSchema,
|
|
4
|
+
BeginTopicStepSchema,
|
|
5
|
+
BeginTopicStepResultSchema,
|
|
6
|
+
} from '@ixo/ixo-transaction/topic-proposals';
|
|
7
|
+
import {
|
|
8
|
+
ActionProposalClaimSchema,
|
|
9
|
+
ActionProposalIdSchema,
|
|
10
|
+
ActionProposalReconcileSchema,
|
|
11
|
+
} from '@ixo/ixo-transaction/action-proposals';
|
|
12
|
+
import { ActionProposalConflict } from '../action-proposals/store';
|
|
1
13
|
import {
|
|
2
14
|
authenticateChannel,
|
|
3
15
|
assertActiveChannelBindingCached,
|
|
@@ -65,6 +77,23 @@ import {
|
|
|
65
77
|
type FeedbackShellOptions,
|
|
66
78
|
} from '../feedback/submit';
|
|
67
79
|
import { z } from 'zod';
|
|
80
|
+
import { parseOutcome } from '@ixo/action-protocol';
|
|
81
|
+
|
|
82
|
+
async function equalOutcomeKeys(
|
|
83
|
+
provided: string | null,
|
|
84
|
+
expected: string | undefined,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
if (!provided || !expected || expected.length < 32 || provided.length > 512)
|
|
87
|
+
return false;
|
|
88
|
+
const digest = async (value: string) =>
|
|
89
|
+
new Uint8Array(
|
|
90
|
+
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)),
|
|
91
|
+
);
|
|
92
|
+
const [a, b] = await Promise.all([digest(provided), digest(expected)]);
|
|
93
|
+
let difference = 0;
|
|
94
|
+
for (let i = 0; i < a.length; i++) difference |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
95
|
+
return difference === 0;
|
|
96
|
+
}
|
|
68
97
|
|
|
69
98
|
const DebugEventBody = z.object({
|
|
70
99
|
type: z
|
|
@@ -217,6 +246,7 @@ export function rateLimitKey(
|
|
|
217
246
|
| 'user'
|
|
218
247
|
| 'poll'
|
|
219
248
|
| 'channel'
|
|
249
|
+
| 'action'
|
|
220
250
|
| 'artifact'
|
|
221
251
|
| 'socket'
|
|
222
252
|
| 'status'
|
|
@@ -424,6 +454,38 @@ export function createShell(
|
|
|
424
454
|
);
|
|
425
455
|
});
|
|
426
456
|
|
|
457
|
+
app.use('/api/internal/actions/outcomes', limitBody(64 * 1024));
|
|
458
|
+
app.post('/api/internal/actions/outcomes', async (c) => {
|
|
459
|
+
if (
|
|
460
|
+
c.env.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
461
|
+
!(await equalOutcomeKeys(
|
|
462
|
+
c.req.header('x-auth-hub-outcome-key') ?? null,
|
|
463
|
+
c.env.AUTH_HUB_ACTION_OUTCOME_KEY,
|
|
464
|
+
))
|
|
465
|
+
)
|
|
466
|
+
return c.json({ code: 'unauthorized' }, 401);
|
|
467
|
+
const body = await c.req.raw.text();
|
|
468
|
+
if (body.length > 64_000) return c.json({ code: 'request_too_large' }, 413);
|
|
469
|
+
try {
|
|
470
|
+
const outcome = parseOutcome(JSON.parse(body));
|
|
471
|
+
await userStub(c.env, outcome.ownerDid).recordHubActionOutcome(outcome);
|
|
472
|
+
return c.json({
|
|
473
|
+
accepted: true,
|
|
474
|
+
eventId: outcome.eventId,
|
|
475
|
+
accountId: outcome.reference.accountId,
|
|
476
|
+
actionId: outcome.reference.id,
|
|
477
|
+
revision: outcome.authorizedRevision,
|
|
478
|
+
digest: outcome.authorizedDigest,
|
|
479
|
+
sequence: outcome.sequence,
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.warn(
|
|
483
|
+
`[hub-actions] outcome refused: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
|
+
);
|
|
485
|
+
return c.json({ code: 'outcome_refused' }, 409);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
427
489
|
app.post('/channels/turn', async (c) => {
|
|
428
490
|
try {
|
|
429
491
|
if (parseChannelServiceDids(c.env.CHANNEL_SERVICE_DID).length === 0)
|
|
@@ -478,6 +540,8 @@ export function createShell(
|
|
|
478
540
|
outcome.status,
|
|
479
541
|
);
|
|
480
542
|
const result = outcome.result;
|
|
543
|
+
if (result.actionOutcomes?.length || result.hubActionOutcomes?.length)
|
|
544
|
+
await assertActiveChannelBindingCached(identity, c.env);
|
|
481
545
|
return c.json(
|
|
482
546
|
result,
|
|
483
547
|
['queued', 'running', 'recovering'].includes(result.status) ? 202 : 200,
|
|
@@ -538,8 +602,15 @@ export function createShell(
|
|
|
538
602
|
if (!limiter) return next();
|
|
539
603
|
const auth = c.get('auth') as AuthResult | undefined;
|
|
540
604
|
if (!auth) return next();
|
|
605
|
+
// The chat lists' status poll and the card routes (Portal's review
|
|
606
|
+
// page) each have their own bucket: polling a card or the run list
|
|
607
|
+
// while chatting must not push turns into 429.
|
|
541
608
|
const scope =
|
|
542
|
-
c.req.method === 'GET' && c.req.path === '/runs/active'
|
|
609
|
+
c.req.method === 'GET' && c.req.path === '/runs/active'
|
|
610
|
+
? 'poll'
|
|
611
|
+
: c.req.path.startsWith('/action-proposals/')
|
|
612
|
+
? 'action'
|
|
613
|
+
: 'user';
|
|
543
614
|
const { success } = await limiter.limit({
|
|
544
615
|
key: rateLimitKey(c.env, scope, auth.userDid),
|
|
545
616
|
});
|
|
@@ -555,6 +626,129 @@ export function createShell(
|
|
|
555
626
|
return next();
|
|
556
627
|
});
|
|
557
628
|
|
|
629
|
+
// What the owner object answers a proposal command with: the card; a
|
|
630
|
+
// conflict (the card or the authority it was sealed on changed: the Portal
|
|
631
|
+
// re-reads and retries, 409); or an authority read that failed and may
|
|
632
|
+
// succeed later (503, with a generic message).
|
|
633
|
+
const ProposalReply = z.discriminatedUnion('ok', [
|
|
634
|
+
z.object({ ok: z.literal(true), proposal: ActionProposalWireSchema }),
|
|
635
|
+
z.object({
|
|
636
|
+
ok: z.literal(false),
|
|
637
|
+
error: z.string(),
|
|
638
|
+
kind: z.enum(['conflict', 'unavailable']).default('conflict'),
|
|
639
|
+
}),
|
|
640
|
+
]);
|
|
641
|
+
const refusalStatus = (kind: 'conflict' | 'unavailable') =>
|
|
642
|
+
kind === 'unavailable' ? 503 : 409;
|
|
643
|
+
|
|
644
|
+
app.get('/action-proposals/:proposalId', async (c) => {
|
|
645
|
+
c.header('Cache-Control', 'private, no-store');
|
|
646
|
+
c.header('Vary', 'Authorization');
|
|
647
|
+
const parsed = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
648
|
+
if (!parsed.success) return c.json({ error: 'Invalid proposal ID' }, 400);
|
|
649
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
650
|
+
const proposal = await userStub(c.env, identity.userDid).actionProposal(
|
|
651
|
+
identity,
|
|
652
|
+
parsed.data,
|
|
653
|
+
);
|
|
654
|
+
if (!proposal) return c.json({ error: 'Proposal not found' }, 404);
|
|
655
|
+
const result = ProposalReply.parse(JSON.parse(proposal));
|
|
656
|
+
return result.ok
|
|
657
|
+
? c.json({ proposal: result.proposal })
|
|
658
|
+
: c.json({ error: result.error }, refusalStatus(result.kind));
|
|
659
|
+
});
|
|
660
|
+
for (const operation of ['claim', 'reject', 'revoke', 'reconcile'] as const) {
|
|
661
|
+
app.post(
|
|
662
|
+
`/action-proposals/:proposalId/${operation}`,
|
|
663
|
+
limitBody(4096),
|
|
664
|
+
async (c) => {
|
|
665
|
+
c.header('Cache-Control', 'private, no-store');
|
|
666
|
+
c.header('Vary', 'Authorization');
|
|
667
|
+
const id = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
668
|
+
const body = await c.req.json().catch(() => null);
|
|
669
|
+
const parsed = (
|
|
670
|
+
operation === 'reconcile'
|
|
671
|
+
? z.union([
|
|
672
|
+
ActionProposalReconcileSchema,
|
|
673
|
+
TopicReceiptReferencesSchema,
|
|
674
|
+
])
|
|
675
|
+
: ActionProposalClaimSchema
|
|
676
|
+
).safeParse(body);
|
|
677
|
+
if (!id.success || !parsed.success)
|
|
678
|
+
return c.json({ error: 'Invalid proposal command' }, 400);
|
|
679
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
680
|
+
try {
|
|
681
|
+
const command =
|
|
682
|
+
operation === 'reconcile'
|
|
683
|
+
? 'v' in parsed.data
|
|
684
|
+
? {
|
|
685
|
+
kind: 'topic-reconcile' as const,
|
|
686
|
+
input: TopicReceiptReferencesSchema.parse(parsed.data),
|
|
687
|
+
}
|
|
688
|
+
: {
|
|
689
|
+
kind: operation,
|
|
690
|
+
...ActionProposalReconcileSchema.parse(parsed.data),
|
|
691
|
+
}
|
|
692
|
+
: {
|
|
693
|
+
kind: operation,
|
|
694
|
+
...ActionProposalClaimSchema.parse(parsed.data),
|
|
695
|
+
};
|
|
696
|
+
const proposal = await userStub(
|
|
697
|
+
c.env,
|
|
698
|
+
identity.userDid,
|
|
699
|
+
).actionProposal(identity, id.data, command);
|
|
700
|
+
if (!proposal) return c.json({ error: 'Proposal not found' }, 404);
|
|
701
|
+
const result = ProposalReply.parse(JSON.parse(proposal));
|
|
702
|
+
return result.ok
|
|
703
|
+
? c.json({ proposal: result.proposal })
|
|
704
|
+
: c.json({ error: result.error }, refusalStatus(result.kind));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (
|
|
707
|
+
error instanceof ActionProposalConflict ||
|
|
708
|
+
(error instanceof Error && error.name === 'ActionProposalConflict')
|
|
709
|
+
)
|
|
710
|
+
return c.json({ error: error.message }, 409);
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
app.post(
|
|
718
|
+
'/action-proposals/:proposalId/begin-step',
|
|
719
|
+
limitBody(4096),
|
|
720
|
+
async (c) => {
|
|
721
|
+
c.header('Cache-Control', 'private, no-store');
|
|
722
|
+
c.header('Vary', 'Authorization');
|
|
723
|
+
const id = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
724
|
+
const body = BeginTopicStepSchema.safeParse(
|
|
725
|
+
await c.req.json().catch(() => null),
|
|
726
|
+
);
|
|
727
|
+
if (!id.success || !body.success)
|
|
728
|
+
return c.json({ error: 'Invalid topic step command' }, 400);
|
|
729
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
730
|
+
const raw = await userStub(c.env, identity.userDid).beginTopicStep(
|
|
731
|
+
identity,
|
|
732
|
+
id.data,
|
|
733
|
+
body.data,
|
|
734
|
+
);
|
|
735
|
+
if (!raw) return c.json({ error: 'Proposal not found' }, 404);
|
|
736
|
+
const response = z
|
|
737
|
+
.discriminatedUnion('ok', [
|
|
738
|
+
z.object({ ok: z.literal(true), result: BeginTopicStepResultSchema }),
|
|
739
|
+
z.object({
|
|
740
|
+
ok: z.literal(false),
|
|
741
|
+
error: z.string(),
|
|
742
|
+
kind: z.enum(['conflict', 'unavailable']).default('conflict'),
|
|
743
|
+
}),
|
|
744
|
+
])
|
|
745
|
+
.parse(JSON.parse(raw));
|
|
746
|
+
return response.ok
|
|
747
|
+
? c.json(response.result)
|
|
748
|
+
: c.json({ error: response.error }, refusalStatus(response.kind));
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
|
|
558
752
|
// --- sessions ----------------------------------------------------------------
|
|
559
753
|
app.post('/sessions', async (c) => {
|
|
560
754
|
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|