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.
Files changed (38) hide show
  1. package/core/agent/AgentToolExecutor.ts +3 -0
  2. package/core/agent/Tool.contract.ts +1 -1
  3. package/core/agent/index.ts +1 -0
  4. package/core/agent/metadata.ts +9 -0
  5. package/core/agent/semantic.ts +8 -1
  6. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  7. package/core/workflow/contracts/types.ts +37 -1
  8. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +10 -1
  9. package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
  10. package/core/workflow/definitions_module/access/Process.references.ts +27 -0
  11. package/core/workflow/definitions_module/events/Definitions.events.ts +16 -0
  12. package/core/workflow/definitions_module/http/Processes.controller.ts +34 -20
  13. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +21 -6
  14. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +48 -21
  15. package/core/workflow/generated/deps.ts +7 -3
  16. package/core/workflow/index.ts +13 -2
  17. package/core/workflow/internal/WorkflowEvents.ts +23 -0
  18. package/core/workflow/internal/http.ts +51 -21
  19. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +4 -1
  20. package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
  21. package/core/workflow/runtime_module/events/Runtime.events.ts +28 -1
  22. package/core/workflow/runtime_module/http/Events.controller.ts +6 -3
  23. package/core/workflow/runtime_module/http/Instances.controller.ts +38 -22
  24. package/core/workflow/runtime_module/internal/ToolStep.ts +2 -1
  25. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +23 -21
  26. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +17 -5
  27. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -11
  28. package/core/workflow/tasks_module/WorkflowTasks.module.ts +4 -1
  29. package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
  30. package/core/workflow/tasks_module/events/Tasks.events.ts +15 -0
  31. package/core/workflow/tasks_module/http/Tasks.controller.ts +33 -22
  32. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +10 -5
  33. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +50 -15
  34. package/library/openapi/codegen.ts +9 -0
  35. package/library/validation/types/ValidatorOptions.ts +10 -0
  36. package/library/workflow/formFields.ts +2 -1
  37. package/package.json +1 -1
  38. 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
  }
@@ -88,6 +88,7 @@ export {
88
88
  type ToolApproval,
89
89
  type ToolMetadata,
90
90
  type ToolActor,
91
+ type ToolDryRun,
91
92
  type ToolOptions,
92
93
  type ToolSideEffect,
93
94
  } from "./metadata";
@@ -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([]),
@@ -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.129.0" }, capabilities: { experimentalApi: true },
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 { Controller, Created, Get, Patch, Post, Put, type HttpContext } from "../../../http";
3
- import { authorize, body, optionalInteger, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
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 come from `WorkflowAccess`. */
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 authorize(ctx, "processes.read");
15
- const archived = ctx.query("archived");
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({ ...pagingQuery(ctx), archived: archived === undefined ? undefined : archived === "true", search: optionalString(ctx.query("search"), "search") });
18
- return { data: page.items, meta: { total: page.total } };
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 authorize(ctx, "processes.manage", { processKey: key });
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 authorize(ctx, "processes.read", { processKey: key });
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 authorize(ctx, "processes.manage", { processKey: key });
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 authorize(ctx, "processes.manage", { processKey: key });
61
- return await this.definitions.saveDraft(key, request.document, actor);
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 authorize(ctx, "processes.publish", { processKey: key });
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 authorize(ctx, "processes.read", { processKey: key });
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 authorize(ctx, "processes.read", { processKey: key });
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
- async tools(ctx: HttpContext) {
90
- await authorize(ctx, "processes.read");
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 and returns every validation finding; `conflict` for a process declared in code. */
73
- abstract saveDraft(key: string, document: unknown, actor: WorkflowActor): Promise<{ readonly issues: readonly ProcessIssue[] }>;
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. */