@springbrand/agent-runtime 0.3.0-alpha.7 → 0.3.0-alpha.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.7",
3
+ "version": "0.3.0-alpha.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -36,7 +36,6 @@ import {
36
36
  type RuntimeUnattendedExecutionGrant,
37
37
  } from "../../submission";
38
38
  import type { ExecutionLevel } from "../../lib/execution-level";
39
- import { pinnedUnattendedExecutionGrant } from "../../submission/unattended";
40
39
  import {
41
40
  HarnessRuntimeConfig,
42
41
  activateRuntimeCandidate,
@@ -798,7 +797,7 @@ export abstract class AgentRuntimeKernel<
798
797
  requestId: submission.requestId,
799
798
  executionLevel: activeRuntime.snapshot.profile.executionLevel,
800
799
  origin: submission.origin,
801
- unattendedExecutionGrant: submission.unattendedExecutionGrant,
800
+ scheduledMaxExecutionLevel: submission.scheduledMaxExecutionLevel ?? undefined,
802
801
  },
803
802
  candidates: harnessInput.candidates,
804
803
  telemetry: this.telemetry,
@@ -826,8 +825,6 @@ export abstract class AgentRuntimeKernel<
826
825
  }
827
826
  },
828
827
  gateTool: harnessInput.platform.gateTool,
829
- authorizeUnattendedExecution:
830
- harnessInput.platform.authorizeUnattendedExecution,
831
828
  onUnattendedFailure: (reason) => {
832
829
  this.db.submissions.recordUnattendedFailure(submissionId, reason);
833
830
  },
@@ -1431,6 +1428,8 @@ export abstract class AgentRuntimeKernel<
1431
1428
  prompt: string,
1432
1429
  options?: {
1433
1430
  idempotencyKey?: string;
1431
+ scheduledMaxExecutionLevel?: ExecutionLevel;
1432
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
1434
1433
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
1435
1434
  },
1436
1435
  ): Promise<SubmissionReceipt> {
@@ -1445,9 +1444,7 @@ export abstract class AgentRuntimeKernel<
1445
1444
  requestId,
1446
1445
  idempotencyKey: options?.idempotencyKey,
1447
1446
  origin: "scheduled",
1448
- unattendedExecutionGrant: options?.unattendedExecutionGrant
1449
- ? pinnedUnattendedExecutionGrant(options.unattendedExecutionGrant)
1450
- : undefined,
1447
+ scheduledMaxExecutionLevel: options?.scheduledMaxExecutionLevel,
1451
1448
  },
1452
1449
  SCHEDULED_STABLE_TIMEOUT_MS,
1453
1450
  );
@@ -264,6 +264,8 @@ export interface RuntimeAgentControls {
264
264
  prompt: string,
265
265
  opts?: {
266
266
  idempotencyKey?: string;
267
+ scheduledMaxExecutionLevel?: import("../../lib/execution-level").ExecutionLevel;
268
+ /** @deprecated Retained for rolling upgrades; ignored by Runtime. */
267
269
  unattendedExecutionGrant?: import("../../submission").RuntimeUnattendedExecutionGrant;
268
270
  },
269
271
  ): Promise<SubmissionReceipt>;
@@ -54,6 +54,9 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
54
54
  if (!submissionColumns.has("origin")) {
55
55
  sql`ALTER TABLE pi_submissions ADD COLUMN origin TEXT NOT NULL DEFAULT 'user'`;
56
56
  }
57
+ if (!submissionColumns.has("scheduled_max_execution_level")) {
58
+ sql`ALTER TABLE pi_submissions ADD COLUMN scheduled_max_execution_level TEXT`;
59
+ }
57
60
  if (!submissionColumns.has("unattended_execution_grant_json")) {
58
61
  sql`ALTER TABLE pi_submissions ADD COLUMN unattended_execution_grant_json TEXT`;
59
62
  }
@@ -6,7 +6,6 @@ import type { RuntimeActivityProjection } from "../conversation/state";
6
6
  import type { ExecutionLevel } from "../lib/execution-level";
7
7
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
8
8
  import type { PiToolCandidate } from "../tools/compiler";
9
- import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
10
9
  import type {
11
10
  RuntimeEventConfirmation,
12
11
  RuntimeLifecycleFact,
@@ -1039,17 +1038,8 @@ export interface RuntimePlatformPort {
1039
1038
  * It is called before dispatch and again at the effect boundary so a grant
1040
1039
  * revocation cannot be hidden by an earlier decision.
1041
1040
  */
1042
- authorizeUnattendedExecution?(request: {
1043
- grant: RuntimeUnattendedExecutionGrant;
1044
- capability: string;
1045
- operation: string;
1046
- object?: unknown;
1047
- toolCallId: string;
1048
- toolName: string;
1049
- input: unknown;
1050
- requiredExecutionLevel: ExecutionLevel;
1051
- signal: AbortSignal;
1052
- }): Promise<void>;
1041
+ /** @deprecated Scheduled execution is authorized locally by execution level. */
1042
+ authorizeUnattendedExecution?(request: unknown): Promise<void>;
1053
1043
  }
1054
1044
 
1055
1045
  /**
@@ -25,3 +25,16 @@ export function maxExecutionLevel(
25
25
  )
26
26
  ]!;
27
27
  }
28
+
29
+ /** The most permissive level shared by an Agent and a Scheduled Task ceiling. */
30
+ export function minExecutionLevel(
31
+ left: ExecutionLevel,
32
+ right: ExecutionLevel,
33
+ ): ExecutionLevel {
34
+ return EXECUTION_LEVELS[
35
+ Math.min(
36
+ EXECUTION_LEVELS.indexOf(left),
37
+ EXECUTION_LEVELS.indexOf(right),
38
+ )
39
+ ]!;
40
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "./lifecycle";
15
15
  import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionOrigin, type SubmissionRepository } from "./repository";
16
16
  import type { RuntimeUnattendedExecutionGrant } from "./contracts";
17
+ import type { ExecutionLevel } from "../lib/execution-level";
17
18
 
18
19
  // Pending records retain identity indefinitely; only recent attempts reserve capacity.
19
20
  const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
@@ -26,6 +27,8 @@ export interface SubmitMessageOptions {
26
27
  regenerate?: boolean;
27
28
  /** `scheduled` marks a Run injection that locks ordinary input until it ends. */
28
29
  origin?: SubmissionOrigin;
30
+ /** Maximum execution level allowed by the Schedule Definition. */
31
+ scheduledMaxExecutionLevel?: ExecutionLevel;
29
32
  /** Trusted Host grant; accepted only for a scheduled Submission. */
30
33
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
31
34
  /** Internal transfer of a durable accepted steer; never a public message option. */
@@ -188,6 +191,7 @@ export class SubmissionAdmission {
188
191
  : null,
189
192
  runId,
190
193
  origin: options.origin,
194
+ scheduledMaxExecutionLevel: options.scheduledMaxExecutionLevel,
191
195
  unattendedExecutionGrant: options.unattendedExecutionGrant,
192
196
  });
193
197
  const admitted = this.options.submissions.find(runId)!;
@@ -1,5 +1,6 @@
1
1
  import type { SqlTaggedTemplate } from "agents/chat";
2
2
  import type { LaneSnapshot } from "@earendil-works/pi-agent-core";
3
+ import type { ExecutionLevel } from "../lib/execution-level";
3
4
  import type { RuntimeUnattendedExecutionGrant } from "./unattended";
4
5
 
5
6
  // Submission 的统一定义见 ./index.ts。
@@ -56,6 +57,7 @@ export interface StoredSubmission {
56
57
  regenerateMessageId: string | null;
57
58
  runId: string | null;
58
59
  origin: SubmissionOrigin;
60
+ scheduledMaxExecutionLevel: ExecutionLevel | null;
59
61
  unattendedExecutionGrant: RuntimeUnattendedExecutionGrant | null;
60
62
  unattendedFailure: string | null;
61
63
  retryable?: boolean;
@@ -82,6 +84,7 @@ export interface NewSubmission {
82
84
  regenerateMessageId?: string | null;
83
85
  runId?: string | null;
84
86
  origin?: SubmissionOrigin;
87
+ scheduledMaxExecutionLevel?: ExecutionLevel;
85
88
  unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
86
89
  }
87
90
 
@@ -105,6 +108,7 @@ type SubmissionRow = {
105
108
  regenerate_message_id: string | null;
106
109
  run_id: string | null;
107
110
  origin: string | null;
111
+ scheduled_max_execution_level: string | null;
108
112
  admission_retryable: number | null;
109
113
  unattended_execution_grant_json: string | null;
110
114
  unattended_failure: string | null;
@@ -136,6 +140,7 @@ function mapRow(row: SubmissionRow): StoredSubmission {
136
140
  regenerateMessageId: row.regenerate_message_id,
137
141
  runId: row.run_id,
138
142
  origin: row.origin === "scheduled" ? "scheduled" : "user",
143
+ scheduledMaxExecutionLevel: row.scheduled_max_execution_level as ExecutionLevel | null,
139
144
  unattendedExecutionGrant: row.unattended_execution_grant_json
140
145
  ? JSON.parse(row.unattended_execution_grant_json) as RuntimeUnattendedExecutionGrant
141
146
  : null,
@@ -173,6 +178,7 @@ export class SubmissionRepository {
173
178
  regenerate_message_id,
174
179
  run_id,
175
180
  origin,
181
+ scheduled_max_execution_level,
176
182
  admission_retryable,
177
183
  unattended_execution_grant_json,
178
184
  unattended_failure
@@ -276,7 +282,7 @@ export class SubmissionRepository {
276
282
  created_at, assembly_revision, assembly_descriptor, runtime_key,
277
283
  assistant_message_id, queued_input_json, queued_ui_message_json,
278
284
  user_message_id, regenerate_message_id,
279
- run_id, origin, unattended_execution_grant_json
285
+ run_id, origin, scheduled_max_execution_level, unattended_execution_grant_json
280
286
  ) VALUES (
281
287
  ${s.submissionId},
282
288
  ${s.requestId},
@@ -294,6 +300,7 @@ export class SubmissionRepository {
294
300
  ${s.regenerateMessageId ?? null},
295
301
  ${s.runId ?? s.submissionId},
296
302
  ${s.origin ?? "user"},
303
+ ${s.scheduledMaxExecutionLevel ?? null},
297
304
  ${s.unattendedExecutionGrant
298
305
  ? JSON.stringify(s.unattendedExecutionGrant)
299
306
  : null}
@@ -87,6 +87,8 @@ export interface PiToolCandidate {
87
87
  readonly alwaysRequiresApproval?: boolean;
88
88
  /** Declares the concrete grant identity derived from actual Tool input. */
89
89
  readonly unattendedExecution?: PiToolUnattendedExecutionSpec;
90
+ /** Explicitly forbids this Tool from unattended Scheduled Runs. */
91
+ readonly unattendedMode?: "forbidden";
90
92
  /** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
91
93
  readonly retry?: "idempotent" | "non-idempotent";
92
94
  readonly summary?: string;
@@ -5,13 +5,13 @@ import type {
5
5
  import type { ExecutionLevel } from "../lib/execution-level";
6
6
  import {
7
7
  EXECUTION_LEVELS,
8
+ minExecutionLevel,
8
9
  requiresExecutionApproval,
9
10
  } from "../lib/execution-level";
10
11
  import type { PiToolCandidate } from "./compiler";
11
12
  import type { ApprovalRepository } from "./approval";
12
13
  import type { ToolInteractionRepository } from "./interaction";
13
14
  import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
14
- import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
15
15
  import { UnattendedExecutionError } from "./unattended-error";
16
16
 
17
17
  export interface HarnessDecisionSubmission {
@@ -19,7 +19,10 @@ export interface HarnessDecisionSubmission {
19
19
  readonly requestId: string;
20
20
  readonly executionLevel: ExecutionLevel;
21
21
  readonly origin?: "user" | "scheduled";
22
- readonly unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant | null;
22
+ /** Maximum level allowed by the owning Schedule Definition. */
23
+ readonly scheduledMaxExecutionLevel?: ExecutionLevel;
24
+ /** @deprecated Retained for rolling upgrades; ignored by the decision gate. */
25
+ readonly unattendedExecutionGrant?: unknown;
23
26
  }
24
27
 
25
28
  export interface HarnessDecisionGateOptions {
@@ -41,17 +44,8 @@ export interface HarnessDecisionGateOptions {
41
44
  readonly requiredExecutionLevel: ExecutionLevel;
42
45
  readonly signal: AbortSignal;
43
46
  }) => void | Promise<void>;
44
- readonly authorizeUnattendedExecution?: (input: {
45
- grant: RuntimeUnattendedExecutionGrant;
46
- capability: string;
47
- operation: string;
48
- object?: unknown;
49
- toolCallId: string;
50
- toolName: string;
51
- input: unknown;
52
- requiredExecutionLevel: ExecutionLevel;
53
- signal: AbortSignal;
54
- }) => void | Promise<void>;
47
+ /** @deprecated Scheduled authorization is local and never calls this hook. */
48
+ readonly authorizeUnattendedExecution?: (input: unknown) => void | Promise<void>;
55
49
  readonly onUnattendedFailure?: (reason: string) => void | Promise<void>;
56
50
  readonly unattendedFailure?: () => string | null | undefined;
57
51
  }
@@ -105,11 +99,9 @@ export class HarnessDecisionGate {
105
99
  if (!(error instanceof UnattendedExecutionError)) throw error;
106
100
  return { block: { reason: error.message, terminate: true } };
107
101
  }
102
+ const effectiveLevel = this.effectiveExecutionLevel();
108
103
  const needsApproval = candidate.alwaysRequiresApproval ||
109
- requiresExecutionApproval(
110
- this.options.submission.executionLevel,
111
- requiredExecutionLevel,
112
- );
104
+ requiresExecutionApproval(effectiveLevel, requiredExecutionLevel);
113
105
  const executionId = `${this.options.submission.submissionId}:${event.toolCallId}`;
114
106
  let unattendedAuthorized = false;
115
107
  try {
@@ -240,6 +232,14 @@ export class HarnessDecisionGate {
240
232
  if (failure) throw new UnattendedExecutionError(failure);
241
233
  }
242
234
 
235
+ private effectiveExecutionLevel(): ExecutionLevel {
236
+ const submission = this.options.submission;
237
+ const ceiling = submission.origin === "scheduled"
238
+ ? submission.scheduledMaxExecutionLevel ?? "high"
239
+ : submission.executionLevel;
240
+ return minExecutionLevel(submission.executionLevel, ceiling);
241
+ }
242
+
243
243
  private async resolveExecutionLevel(candidate: PiToolCandidate, input: unknown): Promise<ExecutionLevel> {
244
244
  this.assertUnattendedActive();
245
245
  try {
@@ -253,7 +253,9 @@ export class HarnessDecisionGate {
253
253
  } catch (error) {
254
254
  if (this.options.submission.origin !== "scheduled") throw error;
255
255
  const reason = error instanceof Error ? error.message : String(error);
256
- return this.denyUnattended(`Unattended execution risk check failed for ${candidate.tool.name}: ${reason}`);
256
+ return this.denyUnattended(
257
+ `SCHEDULED_EXECUTION_RISK_CHECK_FAILED: ${candidate.tool.name}: ${reason}`,
258
+ );
257
259
  }
258
260
  }
259
261
 
@@ -265,52 +267,28 @@ export class HarnessDecisionGate {
265
267
  ): Promise<boolean> {
266
268
  if (this.options.submission.origin !== "scheduled") return false;
267
269
  this.assertUnattendedActive();
268
- const spec = candidate.unattendedExecution;
269
270
  if (candidate.interaction) {
270
271
  return this.denyUnattended(
271
- `Unattended execution requires human input for ${candidate.tool.name}: ${stableInput(input)}`,
272
+ `SCHEDULED_EXECUTION_REQUIRES_INTERACTION: ${candidate.tool.name}`,
272
273
  );
273
274
  }
274
275
  if (candidate.alwaysRequiresApproval) {
275
276
  return this.denyUnattended(
276
- `Unattended execution denied for ${candidate.tool.name}: this operation requires approval for every call`,
277
+ `SCHEDULED_EXECUTION_ALWAYS_REQUIRES_APPROVAL: ${candidate.tool.name}`,
277
278
  );
278
279
  }
279
- if (!spec) {
280
+ if (candidate.unattendedMode === "forbidden") {
280
281
  return this.denyUnattended(
281
- `Unattended execution denied for ${candidate.tool.name}: this Tool has no pre-authorizable operation contract`,
282
+ `SCHEDULED_EXECUTION_FORBIDDEN: ${candidate.tool.name}`,
282
283
  );
283
284
  }
284
- const grant = this.options.submission.unattendedExecutionGrant;
285
- const authorize = this.options.authorizeUnattendedExecution;
286
- if (!grant || !authorize) {
287
- const reason =
288
- `Unattended execution is not authorized for ${spec.capability}.${spec.operation}`;
289
- await this.options.onUnattendedFailure?.(reason);
290
- throw new UnattendedExecutionError(reason);
291
- }
292
- try {
293
- await authorize({
294
- grant,
295
- capability: spec.capability,
296
- operation: spec.operation,
297
- ...(spec.objectForInput
298
- ? { object: await spec.objectForInput(input) }
299
- : {}),
300
- toolCallId: invocation.toolCallId,
301
- toolName: candidate.tool.name,
302
- input,
303
- requiredExecutionLevel,
304
- signal: invocation.signal,
305
- });
306
- return true;
307
- } catch (error) {
308
- const reason = error instanceof Error ? error.message : String(error);
309
- const message =
310
- `Unattended execution denied for ${spec.capability}.${spec.operation}: ${reason}`;
311
- await this.options.onUnattendedFailure?.(message);
312
- throw new UnattendedExecutionError(message, { cause: error });
285
+ const effectiveLevel = this.effectiveExecutionLevel();
286
+ if (requiresExecutionApproval(effectiveLevel, requiredExecutionLevel)) {
287
+ return this.denyUnattended(
288
+ `SCHEDULED_EXECUTION_LEVEL_EXCEEDED: ${candidate.tool.name} requires ${requiredExecutionLevel}; effective level is ${effectiveLevel}`,
289
+ );
313
290
  }
291
+ return true;
314
292
  }
315
293
 
316
294
  private async denyUnattended(reason: string): Promise<never> {