bazis 0.129.0 → 0.131.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/core/agent/AgentToolExecutor.ts +3 -0
- package/core/agent/Tool.contract.ts +1 -1
- package/core/agent/index.ts +1 -0
- package/core/agent/metadata.ts +9 -0
- package/core/agent/semantic.ts +8 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/contracts/types.ts +37 -1
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +10 -1
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
- package/core/workflow/definitions_module/access/Process.references.ts +27 -0
- package/core/workflow/definitions_module/events/Definitions.events.ts +16 -0
- package/core/workflow/definitions_module/http/Processes.controller.ts +34 -20
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +21 -6
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +48 -21
- package/core/workflow/generated/deps.ts +7 -3
- package/core/workflow/index.ts +13 -2
- package/core/workflow/internal/WorkflowEvents.ts +23 -0
- package/core/workflow/internal/http.ts +51 -21
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +4 -1
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
- package/core/workflow/runtime_module/events/Runtime.events.ts +28 -1
- package/core/workflow/runtime_module/http/Events.controller.ts +6 -3
- package/core/workflow/runtime_module/http/Instances.controller.ts +38 -22
- package/core/workflow/runtime_module/internal/ToolStep.ts +2 -1
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +23 -21
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +17 -5
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -11
- package/core/workflow/tasks_module/WorkflowTasks.module.ts +4 -1
- package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
- package/core/workflow/tasks_module/events/Tasks.events.ts +15 -0
- package/core/workflow/tasks_module/http/Tasks.controller.ts +33 -22
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +10 -5
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +50 -15
- package/library/openapi/codegen.ts +9 -0
- package/library/validation/types/ValidatorOptions.ts +10 -0
- package/library/workflow/formFields.ts +2 -1
- package/package.json +1 -1
- package/core/workflow/access/WorkflowAccess.ts +0 -52
|
@@ -58,6 +58,8 @@ export interface AgentToolExecutionContext {
|
|
|
58
58
|
readonly maxAttempts: number;
|
|
59
59
|
readonly idempotencyKey?: string;
|
|
60
60
|
readonly approval: AgentToolExecutionApproval;
|
|
61
|
+
/** On whose behalf the call runs, when the caller says so. */
|
|
62
|
+
readonly actor?: ToolActor;
|
|
61
63
|
readonly metadata: JsonObject;
|
|
62
64
|
readonly signal: AbortSignal;
|
|
63
65
|
}
|
|
@@ -1664,6 +1666,7 @@ export class AgentToolExecutor {
|
|
|
1664
1666
|
maxAttempts: attempt.maxAttempts,
|
|
1665
1667
|
...(attempt.idempotencyKey !== undefined ? { idempotencyKey: attempt.idempotencyKey } : {}),
|
|
1666
1668
|
approval,
|
|
1669
|
+
...(options.actor !== undefined ? { actor: options.actor } : {}),
|
|
1667
1670
|
metadata,
|
|
1668
1671
|
signal,
|
|
1669
1672
|
});
|
|
@@ -15,6 +15,6 @@ export function describeTool(tool: ToolDefinition) {
|
|
|
15
15
|
name: tool.metadata.name, description: tool.metadata.description,
|
|
16
16
|
input: tool.metadata.input ? schema(tool.metadata.input) : undefined,
|
|
17
17
|
output: tool.metadata.output ? schema(tool.metadata.output) : undefined,
|
|
18
|
-
sideEffect: tool.metadata.sideEffect, approval: tool.metadata.approval, timeoutMs: tool.metadata.timeoutMs,
|
|
18
|
+
sideEffect: tool.metadata.sideEffect, approval: tool.metadata.approval, timeoutMs: tool.metadata.timeoutMs, dryRun: tool.metadata.dryRun,
|
|
19
19
|
});
|
|
20
20
|
}
|
package/core/agent/index.ts
CHANGED
package/core/agent/metadata.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ToolActor {
|
|
|
15
15
|
readonly id: string;
|
|
16
16
|
}
|
|
17
17
|
export type ToolApproval = "never" | "policy" | "required";
|
|
18
|
+
/** Whether a Tool can run without changing anything, when a process is tried out (WORKFLOW-002 §5.4). */
|
|
19
|
+
export type ToolDryRun = "supported" | "unsupported";
|
|
18
20
|
|
|
19
21
|
export interface ToolOptions {
|
|
20
22
|
readonly name: string;
|
|
@@ -24,6 +26,8 @@ export interface ToolOptions {
|
|
|
24
26
|
readonly sideEffect?: ToolSideEffect;
|
|
25
27
|
readonly approval?: ToolApproval;
|
|
26
28
|
readonly timeoutMs?: number;
|
|
29
|
+
/** `"unsupported"` by default. */
|
|
30
|
+
readonly dryRun?: ToolDryRun;
|
|
27
31
|
/**
|
|
28
32
|
* Who may call the Tool: `"agent"` for LLM agents, any other id for code that
|
|
29
33
|
* runs Tools itself (`ToolRunner.as("workflow")`). Required and non-empty;
|
|
@@ -42,6 +46,7 @@ export interface ToolMetadata {
|
|
|
42
46
|
readonly sideEffect: ToolSideEffect;
|
|
43
47
|
readonly approval: ToolApproval;
|
|
44
48
|
readonly timeoutMs?: number;
|
|
49
|
+
readonly dryRun: ToolDryRun;
|
|
45
50
|
readonly callers: readonly string[];
|
|
46
51
|
readonly tags: readonly string[];
|
|
47
52
|
}
|
|
@@ -103,6 +108,9 @@ export function defineToolMetadata(_target: object, metadata: object, options: T
|
|
|
103
108
|
if (options.approval !== undefined && options.approval !== "never" && options.approval !== "policy" && options.approval !== "required") {
|
|
104
109
|
throw new AgentSetupError("@Tool.approval must be never, policy or required.");
|
|
105
110
|
}
|
|
111
|
+
if (options.dryRun !== undefined && options.dryRun !== "supported" && options.dryRun !== "unsupported") {
|
|
112
|
+
throw new AgentSetupError("@Tool.dryRun must be supported or unsupported.");
|
|
113
|
+
}
|
|
106
114
|
const normalized: ToolMetadata = Object.freeze({
|
|
107
115
|
name: text(options.name, "@Tool.name"),
|
|
108
116
|
description: text(options.description, "@Tool.description"),
|
|
@@ -111,6 +119,7 @@ export function defineToolMetadata(_target: object, metadata: object, options: T
|
|
|
111
119
|
sideEffect,
|
|
112
120
|
approval: approvalFor(sideEffect, options.approval),
|
|
113
121
|
timeoutMs: positiveInteger(options.timeoutMs, "@Tool.timeoutMs"),
|
|
122
|
+
dryRun: options.dryRun ?? "unsupported",
|
|
114
123
|
// Checked when the module registers the Tool, so a missing list prints as
|
|
115
124
|
// one configuration-error line at start instead of failing the import.
|
|
116
125
|
callers: Array.isArray(options.callers) ? Object.freeze(options.callers.map((caller) => callerId(caller))) : Object.freeze([]),
|
package/core/agent/semantic.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentSemanticError } from "./errors";
|
|
2
|
-
import type { ToolApproval, ToolSideEffect } from "./metadata";
|
|
2
|
+
import type { ToolApproval, ToolDryRun, ToolSideEffect } from "./metadata";
|
|
3
3
|
|
|
4
4
|
const FORBIDDEN_JSON_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
5
5
|
|
|
@@ -162,6 +162,7 @@ export interface AgentToolContract {
|
|
|
162
162
|
readonly sideEffect: ToolSideEffect;
|
|
163
163
|
readonly approval: ToolApproval;
|
|
164
164
|
readonly timeoutMs?: number;
|
|
165
|
+
readonly dryRun: ToolDryRun;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export interface AgentToolContractOptions {
|
|
@@ -172,6 +173,7 @@ export interface AgentToolContractOptions {
|
|
|
172
173
|
readonly sideEffect?: ToolSideEffect;
|
|
173
174
|
readonly approval?: ToolApproval;
|
|
174
175
|
readonly timeoutMs?: number;
|
|
176
|
+
readonly dryRun?: ToolDryRun;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
export interface AgentModelCapabilities {
|
|
@@ -533,6 +535,10 @@ export function agentToolContract(options: AgentToolContractOptions): AgentToolC
|
|
|
533
535
|
if ((sideEffect === "write" || sideEffect === "external") && approval === "never") {
|
|
534
536
|
throw new AgentSemanticError('write/external tools cannot use approval: "never".');
|
|
535
537
|
}
|
|
538
|
+
const dryRun = options.dryRun ?? "unsupported";
|
|
539
|
+
if (dryRun !== "supported" && dryRun !== "unsupported") {
|
|
540
|
+
throw new AgentSemanticError("tool.dryRun must be supported or unsupported.");
|
|
541
|
+
}
|
|
536
542
|
return freezeWithoutUndefined({
|
|
537
543
|
name: requiredName(options.name, "tool.name"),
|
|
538
544
|
description: requiredContentText(options.description, "tool.description"),
|
|
@@ -541,6 +547,7 @@ export function agentToolContract(options: AgentToolContractOptions): AgentToolC
|
|
|
541
547
|
sideEffect,
|
|
542
548
|
approval,
|
|
543
549
|
timeoutMs: optionalPositiveInteger(options.timeoutMs, "tool.timeoutMs"),
|
|
550
|
+
dryRun,
|
|
544
551
|
});
|
|
545
552
|
}
|
|
546
553
|
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.131.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -1,19 +1,42 @@
|
|
|
1
|
+
import type { ListQuery, SortField } from "../../../library/jsonapi";
|
|
2
|
+
|
|
1
3
|
/** Who acts: a user or the system (schedules, events). Steps of an instance run on behalf of who started it. */
|
|
2
4
|
export interface WorkflowActor {
|
|
3
5
|
readonly kind: "user" | "system";
|
|
4
6
|
readonly id: string;
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
/** Where an action came from, for the audit: `null` in its place means code called. */
|
|
10
|
+
export interface WorkflowRequest {
|
|
11
|
+
readonly ip: string | null;
|
|
12
|
+
readonly userAgent: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What the audit events of an action carry besides its actor. */
|
|
16
|
+
export interface WorkflowAuditOptions {
|
|
17
|
+
readonly request?: WorkflowRequest | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What `context.metadata` holds when a process calls a Tool: enough to name the cause in its own events. */
|
|
21
|
+
export interface WorkflowToolMetadata {
|
|
22
|
+
readonly instanceId: string;
|
|
23
|
+
readonly processKey: string;
|
|
24
|
+
readonly nodeId: string;
|
|
25
|
+
readonly partition: string | null;
|
|
26
|
+
readonly test: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
/** A page of a list. */
|
|
8
30
|
export interface WorkflowPage<T> {
|
|
9
31
|
readonly items: readonly T[];
|
|
10
32
|
readonly total: number;
|
|
11
33
|
}
|
|
12
34
|
|
|
13
|
-
/** Paging of the list operations: `limit` 1–200 (50 by default), `offset` from 0. */
|
|
35
|
+
/** Paging of the list operations: `limit` 1–200 (50 by default), `offset` from 0; `sort` replaces the list's own order. */
|
|
14
36
|
export interface WorkflowPaging {
|
|
15
37
|
readonly limit?: number;
|
|
16
38
|
readonly offset?: number;
|
|
39
|
+
readonly sort?: readonly SortField[];
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export function pagingOf(paging: WorkflowPaging): { readonly limit: number; readonly offset: number } {
|
|
@@ -22,6 +45,19 @@ export function pagingOf(paging: WorkflowPaging): { readonly limit: number; read
|
|
|
22
45
|
return { limit: Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 50, offset: Number.isFinite(offset) ? Math.max(offset, 0) : 0 };
|
|
23
46
|
}
|
|
24
47
|
|
|
48
|
+
/** The page of a list operation as `paginate` takes it. */
|
|
49
|
+
export function pageQueryOf(paging: WorkflowPaging, order: SortField): ListQuery {
|
|
50
|
+
const { limit, offset } = pagingOf(paging);
|
|
51
|
+
return {
|
|
52
|
+
sort: paging.sort ?? [order],
|
|
53
|
+
filters: [],
|
|
54
|
+
or: [],
|
|
55
|
+
include: [],
|
|
56
|
+
fields: {},
|
|
57
|
+
page: { limit, offset, size: limit, number: Math.floor(offset / limit) + 1 },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
25
61
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
26
62
|
|
|
27
63
|
/** Whether an id can be a row key; anything else would reach PostgreSQL as an invalid uuid. */
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { AccessModule } from "../../access";
|
|
1
2
|
import { HOSTED_SERVICE, Module, scoped, singleton } from "../../di";
|
|
2
3
|
import { SystemWorkflowClock, WorkflowClock } from "../contracts/WorkflowClock";
|
|
4
|
+
import { WorkflowEvents } from "../internal/WorkflowEvents";
|
|
3
5
|
import { WorkflowTools } from "../internal/WorkflowTools";
|
|
6
|
+
import { definitionsPermissions } from "./access/Definitions.permissions";
|
|
7
|
+
import { ProcessReferences } from "./access/Process.references";
|
|
4
8
|
import { CodeProcessCatalog } from "./code/CodeProcessCatalog";
|
|
5
9
|
import { CodeProcessPublisher } from "./code/CodeProcessPublisher";
|
|
6
10
|
import { ProcessesController } from "./http/Processes.controller";
|
|
@@ -15,15 +19,20 @@ import { WorkflowDefinitions } from "./services/WorkflowDefinitions.service";
|
|
|
15
19
|
* declared in code ([passport](MODULE.md)).
|
|
16
20
|
*/
|
|
17
21
|
@Module({
|
|
22
|
+
imports: [AccessModule],
|
|
23
|
+
permissions: definitionsPermissions,
|
|
24
|
+
references: { "workflow.process": ProcessReferences },
|
|
18
25
|
ormBazis: { context: DefinitionsDbContext, entities: [WfProcess, WfVersion], ownedStore: definitionsStore, registerRepositories: false },
|
|
19
26
|
providers: [
|
|
20
27
|
singleton(WorkflowClock, SystemWorkflowClock),
|
|
21
28
|
singleton(WorkflowTools),
|
|
22
29
|
singleton(CodeProcessCatalog),
|
|
30
|
+
scoped(WorkflowEvents),
|
|
23
31
|
scoped(IWorkflowDefinitions, WorkflowDefinitions),
|
|
32
|
+
scoped(ProcessReferences),
|
|
24
33
|
singleton(HOSTED_SERVICE, CodeProcessPublisher),
|
|
25
34
|
],
|
|
26
35
|
controllers: [ProcessesController],
|
|
27
|
-
exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock],
|
|
36
|
+
exports: [IWorkflowDefinitions, WorkflowTools, WorkflowClock, WorkflowEvents],
|
|
28
37
|
})
|
|
29
38
|
export class WorkflowDefinitionsModule {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { definePermissions } from "../../../access";
|
|
2
|
+
|
|
3
|
+
const process = { ref: "workflow.process", column: "key" };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The rights of the process catalog (WORKFLOW-002 §6.2). A process is one per
|
|
7
|
+
* application, so these have no partition until `WorkflowPartitions` connects
|
|
8
|
+
* processes to partitions (stage 4).
|
|
9
|
+
*/
|
|
10
|
+
export const definitionsPermissions = definePermissions({
|
|
11
|
+
"workflow.processes.read": {
|
|
12
|
+
title: { en: "See processes", ru: "Видеть процессы" },
|
|
13
|
+
on: { process },
|
|
14
|
+
},
|
|
15
|
+
"workflow.processes.manage": {
|
|
16
|
+
title: { en: "Edit processes", ru: "Редактировать процессы" },
|
|
17
|
+
on: { process },
|
|
18
|
+
},
|
|
19
|
+
"workflow.processes.publish": {
|
|
20
|
+
title: { en: "Publish processes", ru: "Публиковать процессы" },
|
|
21
|
+
on: { process },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { References, type ReferenceItem, type ReferencePage } from "../../../access";
|
|
2
|
+
import type { DefinitionsDbContext } from "../model/Definitions.dbContext";
|
|
3
|
+
|
|
4
|
+
/** Reference kind `workflow.process`: what an administrator picks in a condition. */
|
|
5
|
+
export class ProcessReferences extends References {
|
|
6
|
+
constructor(private readonly db: DefinitionsDbContext) {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async exists(ids: readonly string[]): Promise<ReadonlySet<string>> {
|
|
11
|
+
return new Set((await this.describe(ids)).map((item) => item.id));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async describe(ids: readonly string[]): Promise<readonly ReferenceItem[]> {
|
|
15
|
+
if (ids.length === 0) return [];
|
|
16
|
+
const rows = await this.db.processes.asNoTracking().where((p) => p.key.in([...ids])).toList();
|
|
17
|
+
return rows.map((row) => ({ id: row.key, title: row.title }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async search(text: string, _context: unknown, page: ReferencePage): Promise<readonly ReferenceItem[]> {
|
|
21
|
+
const found = text.trim();
|
|
22
|
+
let source = this.db.processes.asNoTracking();
|
|
23
|
+
if (found !== "") source = source.where((p) => p.key.contains(found, { ignoreCase: true }).or(p.title.contains(found, { ignoreCase: true })));
|
|
24
|
+
const rows = await source.orderBy((p) => p.key).skip(page.offset).take(page.limit).toList();
|
|
25
|
+
return rows.map((row) => ({ id: row.key, title: row.title }));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createEventToken } from "../../../kernel";
|
|
2
|
+
import type { WorkflowActor, WorkflowRequest } from "../../contracts/types";
|
|
3
|
+
|
|
4
|
+
export type ProcessChange = "created" | "updated" | "draftSaved" | "published" | "archived" | "restored";
|
|
5
|
+
|
|
6
|
+
/** A process of the catalog changed, for the application's audit. Published after the commit. */
|
|
7
|
+
export interface WorkflowProcessChanged {
|
|
8
|
+
readonly change: ProcessChange;
|
|
9
|
+
readonly key: string;
|
|
10
|
+
/** The version a `published` change made; otherwise `null`. */
|
|
11
|
+
readonly version: number | null;
|
|
12
|
+
readonly actor: WorkflowActor;
|
|
13
|
+
readonly request: WorkflowRequest | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const WORKFLOW_PROCESS_CHANGED = createEventToken<WorkflowProcessChanged>("workflow.process-changed");
|
|
@@ -1,93 +1,107 @@
|
|
|
1
1
|
import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { can } from "../../../access";
|
|
3
|
+
import { Authorize, Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
|
|
4
|
+
import { buildListDocument } from "../../../../library/jsonapi";
|
|
5
|
+
import { body, callerOf, filterOf, listQuery, optionalInteger, optionalString, pagingOfList, param, requiredString } from "../../internal/http";
|
|
4
6
|
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
5
7
|
import { IWorkflowDefinitions } from "../services/IWorkflowDefinitions.service";
|
|
6
8
|
|
|
7
|
-
/** The process catalog over HTTP (WORKFLOW-001 §3); rights
|
|
9
|
+
/** The process catalog over HTTP (WORKFLOW-001 §3); rights are `workflow.processes.*` (WORKFLOW-002 §6.2). */
|
|
8
10
|
@Controller("workflow")
|
|
9
11
|
export class ProcessesController {
|
|
10
12
|
constructor(private readonly definitions: IWorkflowDefinitions) {}
|
|
11
13
|
|
|
12
14
|
@Get("processes")
|
|
15
|
+
@Authorize(can("workflow.processes.read"))
|
|
13
16
|
async list(ctx: HttpContext) {
|
|
14
|
-
await
|
|
15
|
-
const
|
|
17
|
+
const { access } = await callerOf(ctx);
|
|
18
|
+
const list = listQuery(ctx, { filter: { archived: ["eq"], search: ["eq"] }, sort: ["key", "title", "createdAt", "updatedAt"] });
|
|
19
|
+
const archived = filterOf(list, "archived");
|
|
16
20
|
if (archived !== undefined && archived !== "true" && archived !== "false") throw new WorkflowError("invalid_input", "archived must be true or false");
|
|
17
|
-
const page = await this.definitions.list({ ...
|
|
18
|
-
return
|
|
21
|
+
const page = await this.definitions.list({ ...pagingOfList(list), access: await access.filter("workflow.processes.read"), archived: archived === undefined ? undefined : archived === "true", search: filterOf(list, "search") });
|
|
22
|
+
return buildListDocument(page.items, list, page.total, { basePath: ctx.path });
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
@Post("processes")
|
|
26
|
+
@Authorize(can("workflow.processes.manage"))
|
|
22
27
|
async create(ctx: HttpContext) {
|
|
23
28
|
const request = await body(ctx);
|
|
24
29
|
const key = requiredString(request.key, "key", 64);
|
|
25
|
-
const actor = await
|
|
30
|
+
const { actor, request: from } = await callerOf(ctx, "workflow.processes.manage", { process: key });
|
|
26
31
|
const process = await this.definitions.create({
|
|
27
32
|
key,
|
|
28
33
|
title: requiredString(request.title, "title"),
|
|
29
34
|
description: optionalString(request.description, "description", 2000) ?? null,
|
|
30
35
|
retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null,
|
|
31
|
-
}, actor);
|
|
36
|
+
}, actor, { request: from });
|
|
32
37
|
return Created(`${ctx.path}/${process.key}`, process);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
@Get("processes/:key")
|
|
41
|
+
@Authorize(can("workflow.processes.read"))
|
|
36
42
|
async get(ctx: HttpContext) {
|
|
37
43
|
const key = param(ctx, "key");
|
|
38
|
-
await
|
|
44
|
+
await callerOf(ctx, "workflow.processes.read", { process: key });
|
|
39
45
|
return await this.definitions.get(key);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
@Patch("processes/:key")
|
|
49
|
+
@Authorize(can("workflow.processes.manage"))
|
|
43
50
|
async update(ctx: HttpContext) {
|
|
44
51
|
const key = param(ctx, "key");
|
|
45
52
|
const request = await body(ctx);
|
|
46
|
-
const actor = await
|
|
53
|
+
const { actor, request: from } = await callerOf(ctx, "workflow.processes.manage", { process: key });
|
|
47
54
|
if (request.archived !== undefined && typeof request.archived !== "boolean") throw new WorkflowError("invalid_input", "archived must be a boolean");
|
|
48
55
|
return await this.definitions.update(key, {
|
|
49
56
|
...(request.title === undefined ? {} : { title: requiredString(request.title, "title") }),
|
|
50
57
|
...(request.description === undefined ? {} : { description: optionalString(request.description, "description", 2000) ?? null }),
|
|
51
58
|
...(request.retentionDays === undefined ? {} : { retentionDays: optionalInteger(request.retentionDays, "retentionDays") ?? null }),
|
|
52
59
|
...(request.archived === undefined ? {} : { archived: request.archived as boolean }),
|
|
53
|
-
}, actor);
|
|
60
|
+
}, actor, { request: from });
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
@Put("processes/:key/draft")
|
|
64
|
+
@Authorize(can("workflow.processes.manage"))
|
|
57
65
|
async saveDraft(ctx: HttpContext) {
|
|
58
66
|
const key = param(ctx, "key");
|
|
59
67
|
const request = await body(ctx);
|
|
60
|
-
const actor = await
|
|
61
|
-
|
|
68
|
+
const { actor, request: from } = await callerOf(ctx, "workflow.processes.manage", { process: key });
|
|
69
|
+
const expected = optionalString(request.expected, "expected");
|
|
70
|
+
const expectedAt = expected === undefined ? undefined : new Date(expected);
|
|
71
|
+
if (expectedAt !== undefined && Number.isNaN(expectedAt.getTime())) throw new WorkflowError("invalid_input", "expected must be the draftUpdatedAt that was read");
|
|
72
|
+
return await this.definitions.saveDraft(key, request.document, actor, { request: from, ...(expectedAt === undefined ? {} : { expected: expectedAt }) });
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
@Post("processes/:key/publish")
|
|
76
|
+
@Authorize(can("workflow.processes.publish"))
|
|
65
77
|
async publish(ctx: HttpContext) {
|
|
66
78
|
const key = param(ctx, "key");
|
|
67
|
-
const actor = await
|
|
68
|
-
const version = await this.definitions.publish(key, actor);
|
|
79
|
+
const { actor, request } = await callerOf(ctx, "workflow.processes.publish", { process: key });
|
|
80
|
+
const version = await this.definitions.publish(key, actor, { request });
|
|
69
81
|
return Created(`${ctx.path.replace(/\/publish$/, "")}/versions/${version.version}`, version);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
@Get("processes/:key/versions")
|
|
85
|
+
@Authorize(can("workflow.processes.read"))
|
|
73
86
|
async versions(ctx: HttpContext) {
|
|
74
87
|
const key = param(ctx, "key");
|
|
75
|
-
await
|
|
88
|
+
await callerOf(ctx, "workflow.processes.read", { process: key });
|
|
76
89
|
return { data: await this.definitions.versions(key) };
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
@Get("processes/:key/versions/:version")
|
|
93
|
+
@Authorize(can("workflow.processes.read"))
|
|
80
94
|
async version(ctx: HttpContext) {
|
|
81
95
|
const key = param(ctx, "key");
|
|
82
|
-
await
|
|
96
|
+
await callerOf(ctx, "workflow.processes.read", { process: key });
|
|
83
97
|
const version = optionalInteger(param(ctx, "version"), "version");
|
|
84
98
|
if (version === undefined) throw new WorkflowError("invalid_input", "version must be an integer");
|
|
85
99
|
return await this.definitions.version(key, version);
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
@Get("tools")
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
@Authorize(can("workflow.processes.read"))
|
|
104
|
+
async tools() {
|
|
91
105
|
return { data: this.definitions.tools() };
|
|
92
106
|
}
|
|
93
107
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { AccessFilter } from "../../../access";
|
|
1
2
|
import type { AgentToolContract } from "../../../agent";
|
|
2
3
|
import type { ProcessDocument, ProcessIssue } from "../../../../library/workflow";
|
|
3
|
-
import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
|
|
4
|
+
import type { WorkflowActor, WorkflowAuditOptions, WorkflowPage, WorkflowPaging } from "../../contracts/types";
|
|
4
5
|
import type { OwnedCodeProcess } from "../code/codeProcesses";
|
|
5
6
|
|
|
6
7
|
export interface ProcessSummary {
|
|
@@ -53,11 +54,25 @@ export interface UpdateProcess {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export interface ProcessListQuery extends WorkflowPaging {
|
|
57
|
+
/** Which processes the caller may see, from `IAccess`; without it the calling code has decided. */
|
|
58
|
+
readonly access?: AccessFilter;
|
|
56
59
|
readonly archived?: boolean;
|
|
57
60
|
/** Part of the key or title, case-insensitive. */
|
|
58
61
|
readonly search?: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
export interface SaveDraftOptions extends WorkflowAuditOptions {
|
|
65
|
+
/** The `draftUpdatedAt` the caller read: another save since then is `conflict`. Without it the last save wins. */
|
|
66
|
+
readonly expected?: Date;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SavedDraft {
|
|
70
|
+
/** Every validation finding: a draft is stored with errors too. */
|
|
71
|
+
readonly issues: readonly ProcessIssue[];
|
|
72
|
+
/** Goes to `expected` of the next save. */
|
|
73
|
+
readonly draftUpdatedAt: Date;
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
/**
|
|
62
77
|
* The process catalog: drafts and immutable published versions
|
|
63
78
|
* (WORKFLOW-001 §3). Errors are {@link WorkflowError}: `not_found`, `conflict`,
|
|
@@ -66,16 +81,16 @@ export interface ProcessListQuery extends WorkflowPaging {
|
|
|
66
81
|
export abstract class IWorkflowDefinitions {
|
|
67
82
|
abstract list(query?: ProcessListQuery): Promise<WorkflowPage<ProcessSummary>>;
|
|
68
83
|
abstract get(key: string): Promise<ProcessDetails>;
|
|
69
|
-
abstract create(process: CreateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
|
|
84
|
+
abstract create(process: CreateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
|
|
70
85
|
/** A process declared in code takes only `archived`; other fields give `conflict`. */
|
|
71
|
-
abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor): Promise<ProcessDetails>;
|
|
72
|
-
/** Stores the draft even with errors
|
|
73
|
-
abstract saveDraft(key: string, document: unknown, actor: WorkflowActor): Promise<
|
|
86
|
+
abstract update(key: string, changes: UpdateProcess, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<ProcessDetails>;
|
|
87
|
+
/** Stores the draft even with errors; `conflict` for a process declared in code and when `expected` is out of date. */
|
|
88
|
+
abstract saveDraft(key: string, document: unknown, actor: WorkflowActor, options?: SaveDraftOptions): Promise<SavedDraft>;
|
|
74
89
|
/**
|
|
75
90
|
* Validates the draft and stores it as the next version; the same document
|
|
76
91
|
* again returns the latest version. `conflict` for a process declared in code.
|
|
77
92
|
*/
|
|
78
|
-
abstract publish(key: string, actor: WorkflowActor): Promise<VersionDetails>;
|
|
93
|
+
abstract publish(key: string, actor: WorkflowActor, options?: WorkflowAuditOptions): Promise<VersionDetails>;
|
|
79
94
|
abstract versions(key: string): Promise<readonly VersionSummary[]>;
|
|
80
95
|
abstract version(key: string, version: number): Promise<VersionDetails>;
|
|
81
96
|
/** The latest published version of a process that is not archived. */
|