@sanoma-ai/workflows 0.11.16 → 0.11.18
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 +16 -8
- package/dist/call.d.ts +35 -4
- package/dist/call.js +128 -40
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +17 -8
- package/dist/client.js +90 -51
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +25 -7
- package/dist/collection.js +236 -91
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +24 -1
- package/dist/config.js +48 -10
- package/dist/config.js.map +1 -1
- package/dist/db/company.d.ts +20 -0
- package/dist/db/company.js +9 -1
- package/dist/db/company.js.map +1 -1
- package/dist/describe.js +1 -1
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +6 -3
- package/dist/description.js +5 -4
- package/dist/description.js.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/grid.js +3 -2
- package/dist/grid.js.map +1 -1
- package/dist/host-derive.d.ts +13 -8
- package/dist/host-derive.js +26 -13
- package/dist/host-derive.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +7 -0
- package/dist/ledger.js +32 -2
- package/dist/ledger.js.map +1 -1
- package/dist/local-sandbox.d.ts +17 -0
- package/dist/local-sandbox.js +131 -0
- package/dist/local-sandbox.js.map +1 -0
- package/dist/op.d.ts +10 -2
- package/dist/op.js.map +1 -1
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +20 -4
- package/dist/paths.js.map +1 -1
- package/dist/resource.d.ts +7 -1
- package/dist/resource.js +5 -1
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +9 -6
- package/dist/resources.js.map +1 -1
- package/dist/runner-box.d.ts +23 -0
- package/dist/runner-box.js +181 -0
- package/dist/runner-box.js.map +1 -0
- package/dist/runner-protocol.d.ts +73 -0
- package/dist/runner-protocol.js +34 -0
- package/dist/runner-protocol.js.map +1 -0
- package/dist/sandbox-contract.d.ts +109 -0
- package/dist/sandbox-contract.js +26 -0
- package/dist/sandbox-contract.js.map +1 -0
- package/dist/sandbox-runner.cjs +7 -0
- package/dist/scenario.js +16 -7
- package/dist/scenario.js.map +1 -1
- package/dist/schedules.d.ts +11 -3
- package/dist/schedules.js +15 -3
- package/dist/schedules.js.map +1 -1
- package/dist/schema.js +6 -3
- package/dist/schema.js.map +1 -1
- package/dist/secret.d.ts +2 -1
- package/dist/secret.js +3 -2
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +39 -5
- package/dist/shared.js +13 -1
- package/dist/shared.js.map +1 -1
- package/dist/sites.js +10 -8
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +21 -0
- package/dist/source.js.map +1 -1
- package/drizzle/company/0003_ledger_target.sql +2 -0
- package/drizzle/company/meta/0003_snapshot.json +845 -0
- package/drizzle/company/meta/_journal.json +7 -0
- package/package.json +5 -2
- package/src/call.ts +181 -60
- package/src/client.ts +118 -67
- package/src/collection.ts +265 -97
- package/src/config.ts +65 -16
- package/src/db/company.ts +9 -1
- package/src/describe.ts +1 -1
- package/src/description.ts +5 -4
- package/src/errors.ts +3 -2
- package/src/grid.ts +3 -1
- package/src/host-derive.ts +38 -18
- package/src/index.ts +15 -2
- package/src/ledger.ts +42 -2
- package/src/local-sandbox.ts +133 -0
- package/src/op.ts +10 -2
- package/src/paths.ts +22 -4
- package/src/resource.ts +16 -2
- package/src/resources.ts +8 -6
- package/src/runner-box.ts +218 -0
- package/src/runner-protocol.ts +61 -0
- package/src/sandbox-contract.ts +121 -0
- package/src/sandbox-runner.ts +187 -0
- package/src/scenario.ts +16 -6
- package/src/schedules.ts +19 -4
- package/src/schema.ts +14 -3
- package/src/secret.ts +6 -2
- package/src/shared.ts +40 -4
- package/src/sites.ts +11 -7
- package/src/source.ts +14 -0
package/src/client.ts
CHANGED
|
@@ -26,7 +26,19 @@ import {
|
|
|
26
26
|
topicOf,
|
|
27
27
|
withdrawalRefusal,
|
|
28
28
|
} from "./approvals.ts";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
beforeOf,
|
|
31
|
+
type Call,
|
|
32
|
+
callsOf,
|
|
33
|
+
decideCall,
|
|
34
|
+
decideStart,
|
|
35
|
+
decisionOf,
|
|
36
|
+
deniedKeys,
|
|
37
|
+
outputsOf,
|
|
38
|
+
recorded,
|
|
39
|
+
startHoldOf,
|
|
40
|
+
targetOf,
|
|
41
|
+
} from "./call.ts";
|
|
30
42
|
import { type Chats, chatStore } from "./chats.ts";
|
|
31
43
|
import { type Tokens, tokenStore } from "./tokens.ts";
|
|
32
44
|
import { collectionsDriver } from "./collection.ts";
|
|
@@ -97,9 +109,11 @@ import {
|
|
|
97
109
|
errorMessage,
|
|
98
110
|
FOLLOW_WORKFLOW,
|
|
99
111
|
HOST_CHECK_AGENT,
|
|
112
|
+
isBatch,
|
|
100
113
|
isEnded,
|
|
101
114
|
isToken,
|
|
102
115
|
isVisitor,
|
|
116
|
+
LIST_LIMIT,
|
|
103
117
|
refusalOf,
|
|
104
118
|
type RunStatus,
|
|
105
119
|
SCHEDULE_AGENT,
|
|
@@ -452,8 +466,9 @@ export class SanomaClient {
|
|
|
452
466
|
* it as the person, in a run `direct:<actor>:<id>` of the workflow `call` with no approvals. A
|
|
453
467
|
* deny throws `policy_denied` and writes nothing. An allowed call runs the driver once, without
|
|
454
468
|
* DBOS and without a retry (a driver's failure is `driver_failed`). A read returns its output
|
|
455
|
-
* and records nothing. A write is recorded as one `op.called`
|
|
456
|
-
* default ledger in one transaction with
|
|
469
|
+
* and records nothing. A write is recorded as one `op.called` per call it makes (`callsOf`, seq
|
|
470
|
+
* from 0) in that run, and with the default ledger in one transaction with what it saves, so
|
|
471
|
+
* both are written or neither;
|
|
457
472
|
* a write with the same id and input returns the output recorded. When the policy asks for an
|
|
458
473
|
* approval, the call becomes run `call:<actor>:<id>` of the built-in `call` workflow, started
|
|
459
474
|
* as the person, which makes it once approved, and this throws `approval_pending` with its id.
|
|
@@ -482,23 +497,42 @@ export class SanomaClient {
|
|
|
482
497
|
const runId = `direct:${actor.id}:${id}`;
|
|
483
498
|
const heldId = `call:${actor.id}:${id}`;
|
|
484
499
|
const held: RunArgs = { input: { op: op.id, input, ...(selected && { connection: selected }) }, startedBy: actor };
|
|
485
|
-
|
|
500
|
+
// The calls this one makes (a batch's entries), each decided and recorded on its own.
|
|
501
|
+
const calls = callsOf(op, parsed, ops);
|
|
486
502
|
const write = op.effect !== "read";
|
|
487
503
|
// Only an id the caller gave can have been used: a write made, or held, with it settles it.
|
|
504
|
+
// One made and partly recorded is made again, as a retry (the store keeps the records it has).
|
|
505
|
+
let attempt = 1;
|
|
488
506
|
if (write && options.id !== undefined) {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
507
|
+
const made = await this.recordedCall(runId, op, calls, connection);
|
|
508
|
+
if (made && "partial" in made) attempt = 2;
|
|
509
|
+
else {
|
|
510
|
+
const first = made ?? (await this.heldCall(op.id, heldId, held));
|
|
511
|
+
if (first) return first.output;
|
|
512
|
+
}
|
|
492
513
|
}
|
|
493
|
-
const before = await beforeOf(policy, op, parsed, this.config.collections, (get, key) =>
|
|
494
|
-
drivers.get(get)?.(
|
|
495
|
-
{ key },
|
|
496
|
-
{ idempotencyKey: `${runId}:before`, runId, opId: get, attempt: 1, secrets: this.storedSecrets, by: actor.id },
|
|
497
|
-
),
|
|
498
|
-
);
|
|
499
514
|
const run = { id: runId, workflow: CALL_WORKFLOW, approvals: [] };
|
|
500
|
-
const
|
|
501
|
-
|
|
515
|
+
const decisions: RecordedDecision[] = [];
|
|
516
|
+
for (const [i, c] of calls.entries()) {
|
|
517
|
+
const before = await beforeOf(policy, c.op, c.input, this.config.collections, (get, key) =>
|
|
518
|
+
drivers.get(get)?.(
|
|
519
|
+
{ key },
|
|
520
|
+
{
|
|
521
|
+
idempotencyKey: `${runId}:before${i ? `:${i}` : ""}`,
|
|
522
|
+
runId,
|
|
523
|
+
opId: get,
|
|
524
|
+
attempt: 1,
|
|
525
|
+
secrets: this.storedSecrets,
|
|
526
|
+
by: actor.id,
|
|
527
|
+
},
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
decisions.push(decideCall(policy, c.op, c.input, actor, run, { ...before, ...(connection && { connection }) }));
|
|
531
|
+
}
|
|
532
|
+
const decision = decisionOf(op, calls, decisions, this.config.collections);
|
|
533
|
+
if (decision.kind === "deny") {
|
|
534
|
+
throw new PolicyDeniedError(op.id, decision.reason, deniedKeys(op, calls, decisions, this.config.collections));
|
|
535
|
+
}
|
|
502
536
|
if (decision.kind === "approve") {
|
|
503
537
|
await this.start(CALL_WORKFLOW, held.input, { startedBy: actor, runId: heldId });
|
|
504
538
|
// A read held before, whose run has ended since, answers with that run's result.
|
|
@@ -508,7 +542,7 @@ export class SanomaClient {
|
|
|
508
542
|
idempotencyKey: runId,
|
|
509
543
|
runId,
|
|
510
544
|
opId: op.id,
|
|
511
|
-
attempt
|
|
545
|
+
attempt,
|
|
512
546
|
secrets: this.storedSecrets,
|
|
513
547
|
by: actor.id,
|
|
514
548
|
principal: actor,
|
|
@@ -516,26 +550,27 @@ export class SanomaClient {
|
|
|
516
550
|
};
|
|
517
551
|
await this.config.connections.assertCurrent(connection, actor.id);
|
|
518
552
|
if (!write) return outputOf(op, () => fn(parsed, context));
|
|
519
|
-
return this.directWrite({ op, fn, parsed,
|
|
553
|
+
return this.directWrite({ op, fn, parsed, calls, actor, decisions, context });
|
|
520
554
|
}
|
|
521
555
|
|
|
522
556
|
/**
|
|
523
557
|
* A direct write, made once and recorded once: with the default ledger, a collection's write
|
|
524
|
-
* and its
|
|
525
|
-
* otherwise the driver's call, then the
|
|
526
|
-
*
|
|
527
|
-
*
|
|
558
|
+
* and its records in one transaction on the database both are in, so both are kept or neither;
|
|
559
|
+
* otherwise the driver's call, then the records in the config's ledger. A write is recorded as
|
|
560
|
+
* one `op.called` per call it makes (`callsOf`: one, or a batch's entries), numbered from 0.
|
|
561
|
+
* The first records of the call's id stand: a write that finds one already there (two calls
|
|
562
|
+
* with one id at once) is undone, and answers as `recordedCall` does.
|
|
528
563
|
*/
|
|
529
564
|
private async directWrite(call: {
|
|
530
565
|
op: Op;
|
|
531
566
|
fn: (input: unknown, context: CallContext) => Promise<unknown>;
|
|
532
567
|
parsed: unknown;
|
|
533
|
-
|
|
568
|
+
calls: readonly Call[];
|
|
534
569
|
actor: Principal;
|
|
535
|
-
|
|
570
|
+
decisions: readonly RecordedDecision[];
|
|
536
571
|
context: CallContext;
|
|
537
572
|
}): Promise<unknown> {
|
|
538
|
-
const { op, parsed,
|
|
573
|
+
const { op, parsed, calls, actor, decisions, context } = call;
|
|
539
574
|
const { runId } = context;
|
|
540
575
|
const { dataStore, collections, ledger } = this.config;
|
|
541
576
|
const connectionMutation = op.id.startsWith("sanoma.connection.");
|
|
@@ -545,39 +580,45 @@ export class SanomaClient {
|
|
|
545
580
|
? dataStore
|
|
546
581
|
: undefined;
|
|
547
582
|
const at = Date.now();
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
583
|
+
const records = (outcome: (i: number) => { output: unknown } | { error: ErrorInfo }): LedgerRecord[] =>
|
|
584
|
+
calls.map((c, i) => ({
|
|
585
|
+
v: 1,
|
|
586
|
+
app: this.config.appName,
|
|
587
|
+
id: `${runId}:op.called:${i}`,
|
|
588
|
+
runId,
|
|
589
|
+
seq: i,
|
|
590
|
+
at,
|
|
591
|
+
actor,
|
|
592
|
+
workflow: CALL_WORKFLOW,
|
|
593
|
+
type: "op.called",
|
|
594
|
+
op: c.op.id,
|
|
595
|
+
...targetOf(c.op, c.input),
|
|
596
|
+
effect: c.op.effect,
|
|
597
|
+
input: recorded(c.op, c.input),
|
|
598
|
+
decision: decisions[i]!,
|
|
599
|
+
...(context.connection && { connection: context.connection }),
|
|
600
|
+
...outcome(i),
|
|
601
|
+
durationMs: Date.now() - at,
|
|
602
|
+
}));
|
|
603
|
+
const made = (result: unknown) => {
|
|
604
|
+
const outputs = outputsOf(op, result);
|
|
605
|
+
return records((i) => ({ output: recorded(calls[i]!.op, outputs[i]) }));
|
|
606
|
+
};
|
|
566
607
|
/** Runs the call through `fn`; a failure is recorded on the store's own connection, outside any transaction. */
|
|
567
608
|
const output = async (fn: typeof call.fn): Promise<unknown> => {
|
|
568
609
|
try {
|
|
569
610
|
return await outputOf(op, () => fn(parsed, context));
|
|
570
611
|
} catch (err) {
|
|
571
|
-
|
|
612
|
+
for (const record of records(() => ({ error: errorInfo(err) }))) await this.audit.store.append(record);
|
|
572
613
|
throw err;
|
|
573
614
|
}
|
|
574
615
|
};
|
|
575
616
|
if (ledger || !transactionStore) {
|
|
576
617
|
const result = await output(call.fn);
|
|
577
|
-
await this.audit.store.append(record
|
|
618
|
+
for (const record of made(result)) await this.audit.store.append(record);
|
|
578
619
|
return result;
|
|
579
620
|
}
|
|
580
|
-
// The write and its
|
|
621
|
+
// The write and its records in one transaction: the metadata or collection driver runs on the
|
|
581
622
|
// transaction's connection (neither has an env guard), and a record another write of the id
|
|
582
623
|
// was first with rolls the write back, for the recorded output to stand.
|
|
583
624
|
const store = await transactionStore.db();
|
|
@@ -589,33 +630,37 @@ export class SanomaClient {
|
|
|
589
630
|
: collectionsDriver([collections.get(op.resource)!], { db: async () => tx })
|
|
590
631
|
).ops[`${op.resource}.${op.name}`]!;
|
|
591
632
|
const result = await output(fn);
|
|
592
|
-
if (await insertRecord(tx, record
|
|
593
|
-
|
|
633
|
+
for (const record of made(result)) if (!(await insertRecord(tx, record))) tx.rollback();
|
|
634
|
+
return result;
|
|
594
635
|
});
|
|
595
636
|
} catch (err) {
|
|
596
637
|
if (!is(err, TransactionRollbackError)) throw err;
|
|
597
|
-
const first = await this.recordedCall(runId, op
|
|
598
|
-
if (!first) throw err;
|
|
638
|
+
const first = await this.recordedCall(runId, op, calls, context.connection);
|
|
639
|
+
if (!first || "partial" in first) throw err;
|
|
599
640
|
return first.output;
|
|
600
641
|
}
|
|
601
642
|
}
|
|
602
643
|
|
|
603
644
|
/**
|
|
604
|
-
* What the direct write recorded as `runId` returned, when one did
|
|
605
|
-
* when it was of another operation or input,
|
|
645
|
+
* What the direct write recorded as `runId` returned, when one did (a batch's output from its
|
|
646
|
+
* entries' records): refused (`invalid_input`) when it was of another operation or input, its
|
|
647
|
+
* failure again (`driver_failed`) when it failed, and `partial` when fewer records than its
|
|
648
|
+
* calls are there (a batch's, appended one by one outside a transaction, cut short).
|
|
606
649
|
*/
|
|
607
650
|
private async recordedCall(
|
|
608
651
|
runId: string,
|
|
609
|
-
|
|
610
|
-
|
|
652
|
+
op: Op,
|
|
653
|
+
calls: readonly Call[],
|
|
611
654
|
connection?: ConnectionSnapshot,
|
|
612
|
-
): Promise<{ output: unknown } | undefined> {
|
|
613
|
-
const
|
|
614
|
-
if (!
|
|
655
|
+
): Promise<{ output: unknown } | { partial: true } | undefined> {
|
|
656
|
+
const made = (await this.audit.store.read(runId)).filter((r) => r.type === "op.called");
|
|
657
|
+
if (!made.length) return undefined;
|
|
658
|
+
const same = (what: (record: (typeof made)[number], call: Call) => boolean) =>
|
|
659
|
+
made.length <= calls.length && made.every((r, i) => what(r, calls[i]!));
|
|
615
660
|
const differs = [
|
|
616
|
-
...(
|
|
617
|
-
...(sameJson(
|
|
618
|
-
...(sameJson(
|
|
661
|
+
...(same((r, c) => r.op === c.op.id) ? [] : ["op"]),
|
|
662
|
+
...(same((r, c) => sameJson(r.input, recorded(c.op, c.input))) ? [] : ["input"]),
|
|
663
|
+
...(sameJson(made[0]!.connection ?? null, connection ?? null) ? [] : ["connection"]),
|
|
619
664
|
];
|
|
620
665
|
if (differs.length) {
|
|
621
666
|
throw new SanomaError(
|
|
@@ -624,18 +669,16 @@ export class SanomaClient {
|
|
|
624
669
|
{ runId, differs },
|
|
625
670
|
);
|
|
626
671
|
}
|
|
627
|
-
|
|
672
|
+
const failed = made.find((r) => r.error);
|
|
673
|
+
if (failed?.error) {
|
|
628
674
|
throw new SanomaError(
|
|
629
675
|
"driver_failed",
|
|
630
|
-
`${
|
|
631
|
-
{
|
|
632
|
-
runId,
|
|
633
|
-
op: opId,
|
|
634
|
-
error: first.error,
|
|
635
|
-
},
|
|
676
|
+
`${op.id} failed when first called as ${runId}: ${failed.error.message}; call again with another id`,
|
|
677
|
+
{ runId, op: op.id, error: failed.error },
|
|
636
678
|
);
|
|
637
679
|
}
|
|
638
|
-
return {
|
|
680
|
+
if (made.length < calls.length) return { partial: true };
|
|
681
|
+
return { output: isBatch(op) ? made.map((r) => r.output) : made[0]!.output };
|
|
639
682
|
}
|
|
640
683
|
|
|
641
684
|
/**
|
|
@@ -713,6 +756,14 @@ export class SanomaClient {
|
|
|
713
756
|
return this.audit.store.read(runId);
|
|
714
757
|
}
|
|
715
758
|
|
|
759
|
+
/**
|
|
760
|
+
* The calls that acted on `target` (a collection's record, `data:outbound/Acme`; a resource
|
|
761
|
+
* instance) across every run, direct writes among them, newest first, at most `limit`.
|
|
762
|
+
*/
|
|
763
|
+
history(target: string, limit: number = LIST_LIMIT.default): Promise<LedgerRecord[]> {
|
|
764
|
+
return this.audit.store.readByTarget(target, limit);
|
|
765
|
+
}
|
|
766
|
+
|
|
716
767
|
/**
|
|
717
768
|
* The app's runs, newest first: the latest `limit`, or the latest `limit` with a `status`.
|
|
718
769
|
* Only the approvals tell `running` from `waiting`, so those read PENDING runs a page at a
|