@arnilo/prism-server 0.0.24 → 0.0.25

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.25] - 2026-08-06
4
+
5
+ ### Added
6
+ - Agent resume accepts batch `decisions: RunDecision[]` (exclusive with legacy binary `decision`) with boundary validation.
7
+
8
+ ### Changed
9
+ - Released with exact 0.0.25 graph.
10
+
11
+ See [migration guide](../../docs/migration.md) for the 0.0.24 → 0.0.25 notes.
12
+
3
13
  ## [0.0.24] - 2026-08-04
4
14
 
5
15
  ### Added
package/README.md CHANGED
@@ -24,7 +24,7 @@ const response = await handler(new Request("https://api.example.test/prism/agent
24
24
  }));
25
25
  ```
26
26
 
27
- Routes: direct/SSE agent run, cross-replica durable event reconnect through object agent exposures with `events` + `resolveRun`, explicitly selected durable agent status/resume through `agentRuns`, direct/SSE workflow run, durable workflow enqueue/status/cancel/resume/replay, and optional ownership-scoped schedule create/list/pause/resume/trigger/delete. `agentRuns` uses core `createAgentRunLifecycle()`; no lifecycle route exists by default. All bodies, responses, events, queues, concurrency, and timeouts are bounded.
27
+ Routes: direct/SSE agent run, cross-replica durable event reconnect through object agent exposures with `events` + `resolveRun`, explicitly selected durable agent status/resume through `agentRuns`, direct/SSE workflow run, durable workflow enqueue/status/cancel/resume/replay, and optional ownership-scoped schedule create/list/pause/resume/trigger/delete. `agentRuns` uses core `createAgentRunLifecycle()`; no lifecycle route exists by default. Agent resume accepts legacy binary `decision` or batch `decisions: RunDecision[]` (exactly one). All bodies, responses, events, queues, concurrency, and timeouts are bounded.
28
28
 
29
29
  Optional 0.0.14 co-work handlers (mount beside the API handler): `createConversationHandler` (durable user-scoped conversation threads with reconnectable redacted replay — `createConversationService`) and `createArtifactHandler` (artifact revisions, approve/reject review, expiring authorized delivery links — `createArtifactService` over the existing checkpoint store). Both are ownership-scoped and fail closed without authorization.
30
30
 
package/dist/handler.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentRunStateError, assertIdentityActive, assertIdentityMatchesOwnership, } from "@arnilo/prism";
1
+ import { AgentRunStateError, assertIdentityActive, assertIdentityMatchesOwnership, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, } from "@arnilo/prism";
2
2
  import { cancelWorkflowRun, createWorkflowEventBus, enqueueWorkflow, getWorkflowRun, replayWorkflow, resumeWorkflow, runWorkflow, } from "@arnilo/prism-workflows";
3
3
  import { isAdmitOperation } from "./drain.js";
4
4
  import { resolvePrismServerLimits } from "./limits.js";
@@ -558,17 +558,66 @@ function isMessage(value) {
558
558
  const item = value;
559
559
  return ["system", "user", "assistant", "tool"].includes(String(item.role)) && Array.isArray(item.content);
560
560
  }
561
+ const RUN_DECISION_OUTCOMES = new Set(["allow_once", "allow_for_run", "reject_once", "reject_for_run"]);
562
+ const RUN_DECISION_KEYS = new Set(["approvalId", "outcome", "reason", "modifiedArguments", "elicitation"]);
563
+ /** Boundary validation for a client-supplied decision batch; core re-validates under CAS. */
564
+ function readAgentDecisions(value) {
565
+ if (!Array.isArray(value) || value.length === 0 || value.length > HARD_MAX_PENDING_DECISIONS) {
566
+ throw new PrismServerError("decisions must be a non-empty bounded array", 400, "ERR_PRISM_SERVER_RESUME");
567
+ }
568
+ return value.map((entry) => {
569
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
570
+ throw new PrismServerError("decision entry must be an object", 400, "ERR_PRISM_SERVER_RESUME");
571
+ }
572
+ const row = entry;
573
+ if (Object.keys(row).some((key) => !RUN_DECISION_KEYS.has(key))) {
574
+ throw new PrismServerError("decision entry has unknown keys", 400, "ERR_PRISM_SERVER_RESUME");
575
+ }
576
+ if (typeof row.approvalId !== "string" || row.approvalId.length === 0 || row.approvalId.length > 128) {
577
+ throw new PrismServerError("decision approvalId is invalid", 400, "ERR_PRISM_SERVER_RESUME");
578
+ }
579
+ if (typeof row.outcome !== "string" || !RUN_DECISION_OUTCOMES.has(row.outcome)) {
580
+ throw new PrismServerError("decision outcome is invalid", 400, "ERR_PRISM_SERVER_RESUME");
581
+ }
582
+ if (row.reason !== undefined &&
583
+ (typeof row.reason !== "string" || Buffer.byteLength(row.reason, "utf8") > HARD_MAX_DECISION_REASON_BYTES)) {
584
+ throw new PrismServerError("decision reason exceeds limits", 400, "ERR_PRISM_SERVER_RESUME");
585
+ }
586
+ for (const key of ["modifiedArguments", "elicitation"]) {
587
+ const field = row[key];
588
+ if (field === undefined)
589
+ continue;
590
+ if (!field || typeof field !== "object" || Array.isArray(field)) {
591
+ throw new PrismServerError(`decision ${key} must be an object`, 400, "ERR_PRISM_SERVER_RESUME");
592
+ }
593
+ const text = JSON.stringify(field);
594
+ if (text === undefined || Buffer.byteLength(text, "utf8") > HARD_MAX_ELICITATION_BYTES) {
595
+ throw new PrismServerError(`decision ${key} exceeds limits`, 400, "ERR_PRISM_SERVER_RESUME");
596
+ }
597
+ }
598
+ return entry;
599
+ });
600
+ }
561
601
  function readAgentResume(body) {
562
- if (Object.keys(body).some((key) => key !== "decision" && key !== "expectedVersion")) {
602
+ if (Object.keys(body).some((key) => key !== "decision" && key !== "decisions" && key !== "expectedVersion")) {
563
603
  throw new PrismServerError("Invalid agent resume body", 400, "ERR_PRISM_SERVER_RESUME");
564
604
  }
565
- if (body.decision !== "approve" && body.decision !== "deny") {
566
- throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
567
- }
568
605
  if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
569
606
  throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
570
607
  }
571
- return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
608
+ if (body.decision !== undefined && body.decisions !== undefined) {
609
+ throw new PrismServerError("provide exactly one of decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
610
+ }
611
+ if (body.decision !== undefined) {
612
+ if (body.decision !== "approve" && body.decision !== "deny") {
613
+ throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
614
+ }
615
+ return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
616
+ }
617
+ if (body.decisions === undefined) {
618
+ throw new PrismServerError("provide decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
619
+ }
620
+ return { decisions: readAgentDecisions(body.decisions), expectedVersion: Number(body.expectedVersion) };
572
621
  }
573
622
  function readResume(body) {
574
623
  if (body.decision !== "approve" && body.decision !== "deny") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-server",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Optional framework-free Web Request-to-Response handler for explicitly selected Prism agents and workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,8 +25,8 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.24",
29
- "@arnilo/prism-workflows": "0.0.24"
28
+ "@arnilo/prism": "0.0.25",
29
+ "@arnilo/prism-workflows": "0.0.25"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@arnilo/prism": "file:../..",