opencode-heartbeat-approval 0.1.0 → 0.2.1
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/dist/index.js +36 -16
- package/dist/plugin.js +36 -16
- package/package.json +2 -2
- package/src/plugin.ts +43 -14
package/dist/index.js
CHANGED
|
@@ -12333,8 +12333,8 @@ tool.schema = exports_external;
|
|
|
12333
12333
|
// src/plugin.ts
|
|
12334
12334
|
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12335
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
function getRunnerUrl() {
|
|
12337
|
-
return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
12336
|
+
function getRunnerUrl(override) {
|
|
12337
|
+
return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
12338
12338
|
}
|
|
12339
12339
|
function buildHeaders() {
|
|
12340
12340
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -12344,8 +12344,8 @@ function buildHeaders() {
|
|
|
12344
12344
|
}
|
|
12345
12345
|
return headers;
|
|
12346
12346
|
}
|
|
12347
|
-
async function createGate(params) {
|
|
12348
|
-
const resp = await fetch(`${
|
|
12347
|
+
async function createGate(runnerUrl, params) {
|
|
12348
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12349
|
method: "POST",
|
|
12350
12350
|
headers: buildHeaders(),
|
|
12351
12351
|
body: JSON.stringify(params)
|
|
@@ -12359,8 +12359,8 @@ async function createGate(params) {
|
|
|
12359
12359
|
}
|
|
12360
12360
|
return await resp.json();
|
|
12361
12361
|
}
|
|
12362
|
-
async function pollGate() {
|
|
12363
|
-
const resp = await fetch(`${
|
|
12362
|
+
async function pollGate(runnerUrl) {
|
|
12363
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12364
|
method: "GET",
|
|
12365
12365
|
headers: buildHeaders()
|
|
12366
12366
|
});
|
|
@@ -12386,31 +12386,46 @@ function sleep(ms, signal) {
|
|
|
12386
12386
|
var HeartbeatApprovalPlugin = async (_input) => {
|
|
12387
12387
|
return {
|
|
12388
12388
|
tool: {
|
|
12389
|
-
|
|
12390
|
-
description: `Request human
|
|
12389
|
+
request_human_input: tool({
|
|
12390
|
+
description: `Request human input before proceeding \u2014 either approval (permission to act) or assistance (human help needed).
|
|
12391
12391
|
|
|
12392
|
-
Call this tool when you need explicit human confirmation
|
|
12392
|
+
Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
|
|
12393
12393
|
|
|
12394
|
-
|
|
12394
|
+
The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
|
|
12395
12395
|
|
|
12396
|
-
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
|
|
12396
|
+
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
|
|
12397
12397
|
args: {
|
|
12398
|
-
prompt: tool.schema.string().describe("What you need
|
|
12398
|
+
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
|
+
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12399
12400
|
artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
|
|
12400
|
-
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12401
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)"),
|
|
12402
|
+
runner_url: tool.schema.string().optional().describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups.")
|
|
12401
12403
|
},
|
|
12402
12404
|
async execute(args, ctx) {
|
|
12403
12405
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
12406
|
+
const validTypes = ["approval", "assistance"];
|
|
12407
|
+
if (args.type !== undefined && !validTypes.includes(args.type)) {
|
|
12408
|
+
return JSON.stringify({
|
|
12409
|
+
status: "error",
|
|
12410
|
+
type: null,
|
|
12411
|
+
error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
|
|
12412
|
+
approval_id: null
|
|
12413
|
+
});
|
|
12414
|
+
}
|
|
12415
|
+
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
+
const resolvedUrl = getRunnerUrl(args.runner_url);
|
|
12404
12417
|
let gateResult;
|
|
12405
12418
|
try {
|
|
12406
|
-
gateResult = await createGate({
|
|
12419
|
+
gateResult = await createGate(resolvedUrl, {
|
|
12407
12420
|
prompt: args.prompt,
|
|
12408
12421
|
artifacts,
|
|
12409
|
-
deadline_hours: args.deadline_hours
|
|
12422
|
+
deadline_hours: args.deadline_hours,
|
|
12423
|
+
type: requestType
|
|
12410
12424
|
});
|
|
12411
12425
|
} catch (err) {
|
|
12412
12426
|
return JSON.stringify({
|
|
12413
12427
|
status: "unavailable",
|
|
12428
|
+
type: requestType,
|
|
12414
12429
|
error: err instanceof Error ? err.message : String(err),
|
|
12415
12430
|
approval_id: null
|
|
12416
12431
|
});
|
|
@@ -12418,6 +12433,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12418
12433
|
if ("error" in gateResult) {
|
|
12419
12434
|
return JSON.stringify({
|
|
12420
12435
|
status: "conflict",
|
|
12436
|
+
type: requestType,
|
|
12421
12437
|
error: gateResult.message,
|
|
12422
12438
|
existing_approval_id: gateResult.existing_approval_id,
|
|
12423
12439
|
approval_id: null
|
|
@@ -12428,6 +12444,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12428
12444
|
if (ctx.abort?.aborted) {
|
|
12429
12445
|
return JSON.stringify({
|
|
12430
12446
|
status: "cancelled",
|
|
12447
|
+
type: requestType,
|
|
12431
12448
|
response: null,
|
|
12432
12449
|
approval_id: approvalId
|
|
12433
12450
|
});
|
|
@@ -12435,7 +12452,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12435
12452
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12436
12453
|
let poll;
|
|
12437
12454
|
try {
|
|
12438
|
-
poll = await pollGate();
|
|
12455
|
+
poll = await pollGate(resolvedUrl);
|
|
12439
12456
|
} catch {
|
|
12440
12457
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12441
12458
|
continue;
|
|
@@ -12443,6 +12460,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12443
12460
|
if (poll.status === "approved") {
|
|
12444
12461
|
return JSON.stringify({
|
|
12445
12462
|
status: "approved",
|
|
12463
|
+
type: requestType,
|
|
12446
12464
|
response: poll.response ?? null,
|
|
12447
12465
|
approval_id: approvalId
|
|
12448
12466
|
});
|
|
@@ -12450,6 +12468,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12450
12468
|
if (poll.status === "rejected") {
|
|
12451
12469
|
return JSON.stringify({
|
|
12452
12470
|
status: "rejected",
|
|
12471
|
+
type: requestType,
|
|
12453
12472
|
response: poll.response ?? null,
|
|
12454
12473
|
approval_id: approvalId
|
|
12455
12474
|
});
|
|
@@ -12457,6 +12476,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12457
12476
|
if (poll.status === "expired") {
|
|
12458
12477
|
return JSON.stringify({
|
|
12459
12478
|
status: "expired",
|
|
12479
|
+
type: requestType,
|
|
12460
12480
|
response: null,
|
|
12461
12481
|
approval_id: approvalId
|
|
12462
12482
|
});
|
package/dist/plugin.js
CHANGED
|
@@ -12333,8 +12333,8 @@ tool.schema = exports_external;
|
|
|
12333
12333
|
// src/plugin.ts
|
|
12334
12334
|
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12335
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
function getRunnerUrl() {
|
|
12337
|
-
return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
12336
|
+
function getRunnerUrl(override) {
|
|
12337
|
+
return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
12338
12338
|
}
|
|
12339
12339
|
function buildHeaders() {
|
|
12340
12340
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -12344,8 +12344,8 @@ function buildHeaders() {
|
|
|
12344
12344
|
}
|
|
12345
12345
|
return headers;
|
|
12346
12346
|
}
|
|
12347
|
-
async function createGate(params) {
|
|
12348
|
-
const resp = await fetch(`${
|
|
12347
|
+
async function createGate(runnerUrl, params) {
|
|
12348
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12349
|
method: "POST",
|
|
12350
12350
|
headers: buildHeaders(),
|
|
12351
12351
|
body: JSON.stringify(params)
|
|
@@ -12359,8 +12359,8 @@ async function createGate(params) {
|
|
|
12359
12359
|
}
|
|
12360
12360
|
return await resp.json();
|
|
12361
12361
|
}
|
|
12362
|
-
async function pollGate() {
|
|
12363
|
-
const resp = await fetch(`${
|
|
12362
|
+
async function pollGate(runnerUrl) {
|
|
12363
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12364
|
method: "GET",
|
|
12365
12365
|
headers: buildHeaders()
|
|
12366
12366
|
});
|
|
@@ -12386,31 +12386,46 @@ function sleep(ms, signal) {
|
|
|
12386
12386
|
var HeartbeatApprovalPlugin = async (_input) => {
|
|
12387
12387
|
return {
|
|
12388
12388
|
tool: {
|
|
12389
|
-
|
|
12390
|
-
description: `Request human
|
|
12389
|
+
request_human_input: tool({
|
|
12390
|
+
description: `Request human input before proceeding \u2014 either approval (permission to act) or assistance (human help needed).
|
|
12391
12391
|
|
|
12392
|
-
Call this tool when you need explicit human confirmation
|
|
12392
|
+
Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
|
|
12393
12393
|
|
|
12394
|
-
|
|
12394
|
+
The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
|
|
12395
12395
|
|
|
12396
|
-
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
|
|
12396
|
+
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
|
|
12397
12397
|
args: {
|
|
12398
|
-
prompt: tool.schema.string().describe("What you need
|
|
12398
|
+
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
|
+
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12399
12400
|
artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
|
|
12400
|
-
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12401
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)"),
|
|
12402
|
+
runner_url: tool.schema.string().optional().describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups.")
|
|
12401
12403
|
},
|
|
12402
12404
|
async execute(args, ctx) {
|
|
12403
12405
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
12406
|
+
const validTypes = ["approval", "assistance"];
|
|
12407
|
+
if (args.type !== undefined && !validTypes.includes(args.type)) {
|
|
12408
|
+
return JSON.stringify({
|
|
12409
|
+
status: "error",
|
|
12410
|
+
type: null,
|
|
12411
|
+
error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
|
|
12412
|
+
approval_id: null
|
|
12413
|
+
});
|
|
12414
|
+
}
|
|
12415
|
+
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
+
const resolvedUrl = getRunnerUrl(args.runner_url);
|
|
12404
12417
|
let gateResult;
|
|
12405
12418
|
try {
|
|
12406
|
-
gateResult = await createGate({
|
|
12419
|
+
gateResult = await createGate(resolvedUrl, {
|
|
12407
12420
|
prompt: args.prompt,
|
|
12408
12421
|
artifacts,
|
|
12409
|
-
deadline_hours: args.deadline_hours
|
|
12422
|
+
deadline_hours: args.deadline_hours,
|
|
12423
|
+
type: requestType
|
|
12410
12424
|
});
|
|
12411
12425
|
} catch (err) {
|
|
12412
12426
|
return JSON.stringify({
|
|
12413
12427
|
status: "unavailable",
|
|
12428
|
+
type: requestType,
|
|
12414
12429
|
error: err instanceof Error ? err.message : String(err),
|
|
12415
12430
|
approval_id: null
|
|
12416
12431
|
});
|
|
@@ -12418,6 +12433,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12418
12433
|
if ("error" in gateResult) {
|
|
12419
12434
|
return JSON.stringify({
|
|
12420
12435
|
status: "conflict",
|
|
12436
|
+
type: requestType,
|
|
12421
12437
|
error: gateResult.message,
|
|
12422
12438
|
existing_approval_id: gateResult.existing_approval_id,
|
|
12423
12439
|
approval_id: null
|
|
@@ -12428,6 +12444,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12428
12444
|
if (ctx.abort?.aborted) {
|
|
12429
12445
|
return JSON.stringify({
|
|
12430
12446
|
status: "cancelled",
|
|
12447
|
+
type: requestType,
|
|
12431
12448
|
response: null,
|
|
12432
12449
|
approval_id: approvalId
|
|
12433
12450
|
});
|
|
@@ -12435,7 +12452,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12435
12452
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12436
12453
|
let poll;
|
|
12437
12454
|
try {
|
|
12438
|
-
poll = await pollGate();
|
|
12455
|
+
poll = await pollGate(resolvedUrl);
|
|
12439
12456
|
} catch {
|
|
12440
12457
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12441
12458
|
continue;
|
|
@@ -12443,6 +12460,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12443
12460
|
if (poll.status === "approved") {
|
|
12444
12461
|
return JSON.stringify({
|
|
12445
12462
|
status: "approved",
|
|
12463
|
+
type: requestType,
|
|
12446
12464
|
response: poll.response ?? null,
|
|
12447
12465
|
approval_id: approvalId
|
|
12448
12466
|
});
|
|
@@ -12450,6 +12468,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12450
12468
|
if (poll.status === "rejected") {
|
|
12451
12469
|
return JSON.stringify({
|
|
12452
12470
|
status: "rejected",
|
|
12471
|
+
type: requestType,
|
|
12453
12472
|
response: poll.response ?? null,
|
|
12454
12473
|
approval_id: approvalId
|
|
12455
12474
|
});
|
|
@@ -12457,6 +12476,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12457
12476
|
if (poll.status === "expired") {
|
|
12458
12477
|
return JSON.stringify({
|
|
12459
12478
|
status: "expired",
|
|
12479
|
+
type: requestType,
|
|
12460
12480
|
response: null,
|
|
12461
12481
|
approval_id: approvalId
|
|
12462
12482
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-heartbeat-approval",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "OpenCode plugin providing
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "OpenCode plugin providing request_human_input MCP tool for Heartbeat pipeline (approval + assistance)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
package/src/plugin.ts
CHANGED
|
@@ -8,16 +8,18 @@ interface CreateResponse {
|
|
|
8
8
|
approval_id: string;
|
|
9
9
|
status: string;
|
|
10
10
|
created: boolean;
|
|
11
|
+
type?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface PollResponse {
|
|
14
15
|
approval_id: string | null;
|
|
15
16
|
status: string;
|
|
16
17
|
response?: string;
|
|
18
|
+
type?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function getRunnerUrl(): string {
|
|
20
|
-
return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
21
|
+
function getRunnerUrl(override?: string): string {
|
|
22
|
+
return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function buildHeaders(): Record<string, string> {
|
|
@@ -35,13 +37,14 @@ interface ConflictResponse {
|
|
|
35
37
|
existing_approval_id: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
async function createGate(params: {
|
|
40
|
+
async function createGate(runnerUrl: string, params: {
|
|
39
41
|
prompt: string;
|
|
40
42
|
artifacts?: string[];
|
|
41
43
|
deadline_hours?: number;
|
|
42
44
|
phase?: string;
|
|
45
|
+
type?: string;
|
|
43
46
|
}): Promise<CreateResponse | ConflictResponse> {
|
|
44
|
-
const resp = await fetch(`${
|
|
47
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
45
48
|
method: "POST",
|
|
46
49
|
headers: buildHeaders(),
|
|
47
50
|
body: JSON.stringify(params),
|
|
@@ -56,8 +59,8 @@ async function createGate(params: {
|
|
|
56
59
|
return (await resp.json()) as CreateResponse;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
async function pollGate(): Promise<PollResponse> {
|
|
60
|
-
const resp = await fetch(`${
|
|
62
|
+
async function pollGate(runnerUrl: string): Promise<PollResponse> {
|
|
63
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
61
64
|
method: "GET",
|
|
62
65
|
headers: buildHeaders(),
|
|
63
66
|
});
|
|
@@ -79,18 +82,22 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
79
82
|
export const HeartbeatApprovalPlugin = async (_input: PluginInput) => {
|
|
80
83
|
return {
|
|
81
84
|
tool: {
|
|
82
|
-
|
|
83
|
-
description: `Request human
|
|
85
|
+
request_human_input: tool({
|
|
86
|
+
description: `Request human input before proceeding — either approval (permission to act) or assistance (human help needed).
|
|
84
87
|
|
|
85
|
-
Call this tool when you need explicit human confirmation
|
|
88
|
+
Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
|
|
88
91
|
|
|
89
|
-
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
|
|
92
|
+
Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
|
|
90
93
|
args: {
|
|
91
94
|
prompt: tool.schema
|
|
92
95
|
.string()
|
|
93
|
-
.describe("What you need
|
|
96
|
+
.describe("What you need from the human — be specific and concise"),
|
|
97
|
+
type: tool.schema
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
94
101
|
artifacts: tool.schema
|
|
95
102
|
.string()
|
|
96
103
|
.optional()
|
|
@@ -99,22 +106,39 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
99
106
|
.number()
|
|
100
107
|
.optional()
|
|
101
108
|
.describe("Hours before the request expires (default: 24, max: 168)"),
|
|
109
|
+
runner_url: tool.schema
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups."),
|
|
102
113
|
},
|
|
103
114
|
async execute(args, ctx) {
|
|
104
115
|
const artifacts = args.artifacts
|
|
105
116
|
? args.artifacts.split(",").map((s: string) => s.trim()).filter(Boolean)
|
|
106
117
|
: undefined;
|
|
118
|
+
const validTypes = ["approval", "assistance"];
|
|
119
|
+
if (args.type !== undefined && !validTypes.includes(args.type)) {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
status: "error",
|
|
122
|
+
type: null,
|
|
123
|
+
error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
|
|
124
|
+
approval_id: null,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
|
|
128
|
+
const resolvedUrl = getRunnerUrl(args.runner_url);
|
|
107
129
|
|
|
108
130
|
let gateResult: CreateResponse | ConflictResponse;
|
|
109
131
|
try {
|
|
110
|
-
gateResult = await createGate({
|
|
132
|
+
gateResult = await createGate(resolvedUrl, {
|
|
111
133
|
prompt: args.prompt,
|
|
112
134
|
artifacts,
|
|
113
135
|
deadline_hours: args.deadline_hours,
|
|
136
|
+
type: requestType,
|
|
114
137
|
});
|
|
115
138
|
} catch (err) {
|
|
116
139
|
return JSON.stringify({
|
|
117
140
|
status: "unavailable",
|
|
141
|
+
type: requestType,
|
|
118
142
|
error: err instanceof Error ? err.message : String(err),
|
|
119
143
|
approval_id: null,
|
|
120
144
|
});
|
|
@@ -123,6 +147,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
123
147
|
if ("error" in gateResult) {
|
|
124
148
|
return JSON.stringify({
|
|
125
149
|
status: "conflict",
|
|
150
|
+
type: requestType,
|
|
126
151
|
error: gateResult.message,
|
|
127
152
|
existing_approval_id: gateResult.existing_approval_id,
|
|
128
153
|
approval_id: null,
|
|
@@ -135,6 +160,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
135
160
|
if (ctx.abort?.aborted) {
|
|
136
161
|
return JSON.stringify({
|
|
137
162
|
status: "cancelled",
|
|
163
|
+
type: requestType,
|
|
138
164
|
response: null,
|
|
139
165
|
approval_id: approvalId,
|
|
140
166
|
});
|
|
@@ -144,7 +170,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
144
170
|
|
|
145
171
|
let poll: PollResponse;
|
|
146
172
|
try {
|
|
147
|
-
poll = await pollGate();
|
|
173
|
+
poll = await pollGate(resolvedUrl);
|
|
148
174
|
} catch {
|
|
149
175
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
150
176
|
continue;
|
|
@@ -153,6 +179,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
153
179
|
if (poll.status === "approved") {
|
|
154
180
|
return JSON.stringify({
|
|
155
181
|
status: "approved",
|
|
182
|
+
type: requestType,
|
|
156
183
|
response: poll.response ?? null,
|
|
157
184
|
approval_id: approvalId,
|
|
158
185
|
});
|
|
@@ -161,6 +188,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
161
188
|
if (poll.status === "rejected") {
|
|
162
189
|
return JSON.stringify({
|
|
163
190
|
status: "rejected",
|
|
191
|
+
type: requestType,
|
|
164
192
|
response: poll.response ?? null,
|
|
165
193
|
approval_id: approvalId,
|
|
166
194
|
});
|
|
@@ -169,6 +197,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
169
197
|
if (poll.status === "expired") {
|
|
170
198
|
return JSON.stringify({
|
|
171
199
|
status: "expired",
|
|
200
|
+
type: requestType,
|
|
172
201
|
response: null,
|
|
173
202
|
approval_id: approvalId,
|
|
174
203
|
});
|