@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.
Files changed (46) hide show
  1. package/README.md +15 -13
  2. package/package.json +10 -4
  3. package/src/action-proposals/policy.ts +54 -0
  4. package/src/action-proposals/reconcile.ts +87 -0
  5. package/src/action-proposals/store.ts +863 -0
  6. package/src/action-proposals/topic-budget.ts +84 -0
  7. package/src/action-proposals/topic-chain.ts +272 -0
  8. package/src/action-proposals/topic-effects.ts +44 -0
  9. package/src/action-proposals/topic-errors.ts +79 -0
  10. package/src/action-proposals/topic-history.ts +267 -0
  11. package/src/action-proposals/topic-matrix.ts +292 -0
  12. package/src/action-proposals/topic-prepare.ts +299 -0
  13. package/src/action-proposals/topic-store.ts +999 -0
  14. package/src/action-proposals/topic-verify.ts +279 -0
  15. package/src/action-proposals/verify.ts +223 -0
  16. package/src/channels/auth.ts +6 -1
  17. package/src/channels/contract.ts +4 -0
  18. package/src/channels/turns.ts +1 -0
  19. package/src/core/env.ts +93 -6
  20. package/src/core/main-agent-types.ts +8 -0
  21. package/src/core/main-agent.ts +2 -0
  22. package/src/core/runtime-context.ts +12 -0
  23. package/src/delivery/plan.ts +43 -2
  24. package/src/delivery/schema.ts +25 -1
  25. package/src/delivery/types.ts +13 -2
  26. package/src/do/ambient.ts +6 -0
  27. package/src/do/contracts.ts +39 -0
  28. package/src/do/user-oracle-do.ts +1275 -8
  29. package/src/hub-actions/client.ts +163 -0
  30. package/src/hub-actions/request.ts +110 -0
  31. package/src/hub-actions/store.ts +485 -0
  32. package/src/matrix/gateway-do.ts +176 -0
  33. package/src/matrix/reply-parts.ts +8 -2
  34. package/src/memory/session-history-indexer.ts +30 -3
  35. package/src/memory/user-context-fetcher.ts +36 -12
  36. package/src/plugin-api/types.ts +54 -0
  37. package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
  38. package/src/plugins/action-proposals/evidence.ts +67 -0
  39. package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
  40. package/src/plugins/index.ts +6 -0
  41. package/src/plugins/memory/index.ts +4 -0
  42. package/src/plugins/memory/memory-entity.ts +49 -0
  43. package/src/plugins/memory/memory-tools.ts +7 -4
  44. package/src/plugins/memory/memory-ucan.ts +10 -1
  45. package/src/plugins/memory/memory.plugin.ts +22 -7
  46. 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' ? 'poll' : 'user';
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);