@syengup/friday-channel-next 0.1.39 → 1.0.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/dist/index.js +59 -1
- package/dist/src/agent/subagent-registry.d.ts +4 -0
- package/dist/src/agent/subagent-registry.js +1 -1
- package/dist/src/approval/friday-approval-capability.d.ts +44 -0
- package/dist/src/approval/friday-approval-capability.js +174 -0
- package/dist/src/channel.js +22 -0
- package/dist/src/codex-reasoning-config.d.ts +11 -0
- package/dist/src/codex-reasoning-config.js +83 -0
- package/dist/src/friday-session.d.ts +4 -0
- package/dist/src/friday-session.js +59 -1
- package/dist/src/http/handlers/agents-list.js +5 -1
- package/dist/src/http/handlers/approvals.d.ts +9 -0
- package/dist/src/http/handlers/approvals.js +54 -0
- package/dist/src/http/handlers/messages.js +19 -1
- package/dist/src/http/server.js +6 -0
- package/dist/src/http 2/middleware/auth.d.ts +13 -0
- package/dist/src/http 2/middleware/auth.js +29 -0
- package/dist/src/http 2/middleware/body.d.ts +2 -0
- package/dist/src/http 2/middleware/body.js +24 -0
- package/dist/src/http 2/middleware/cors.d.ts +2 -0
- package/dist/src/http 2/middleware/cors.js +11 -0
- package/dist/src/sse/emitter.d.ts +1 -1
- package/index.ts +61 -0
- package/package.json +15 -14
- package/src/agent/subagent-registry.ts +3 -1
- package/src/approval/friday-approval-capability.test.ts +78 -0
- package/src/approval/friday-approval-capability.ts +227 -0
- package/src/channel.ts +25 -1
- package/src/codex-reasoning-config.test.ts +28 -0
- package/src/codex-reasoning-config.ts +82 -0
- package/src/e2e/subagent.e2e.test.ts +6 -0
- package/src/friday-session.forward-agent.test.ts +127 -0
- package/src/friday-session.ts +76 -1
- package/src/http/handlers/agents-list.test.ts +28 -0
- package/src/http/handlers/agents-list.ts +5 -1
- package/src/http/handlers/approvals.ts +61 -0
- package/src/http/handlers/messages.ts +23 -1
- package/src/http/server.ts +7 -0
- package/src/sse/emitter.ts +2 -1
- package/dist/src/health/self-health.d.ts +0 -39
- package/dist/src/health/self-health.js +0 -174
- package/dist/src/http/handlers/sessions-delete.d.ts +0 -2
- package/dist/src/http/handlers/sessions-delete.js +0 -49
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
|
|
2
|
+
import { readJsonBody } from "../middleware/body.js";
|
|
3
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
5
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
6
|
+
import { createFridayNextLogger } from "../../logging.js";
|
|
7
|
+
const VALID_DECISIONS = new Set(["allow-once", "allow-always", "deny"]);
|
|
8
|
+
/**
|
|
9
|
+
* POST /friday-next/approvals/{approvalId}
|
|
10
|
+
* Body: { decision: "allow-once" | "allow-always" | "deny", deviceId?: string }
|
|
11
|
+
*
|
|
12
|
+
* Submits the app user's decision for a pending exec/plugin approval back to the gateway. The bearer
|
|
13
|
+
* token gates auth (the device owner is the approver); the gateway then resumes / aborts the run.
|
|
14
|
+
*/
|
|
15
|
+
export async function handleApprovalDecision(req, res, approvalId) {
|
|
16
|
+
const log = createFridayNextLogger("approvals");
|
|
17
|
+
const json = (status, body) => {
|
|
18
|
+
res.statusCode = status;
|
|
19
|
+
res.setHeader("Content-Type", "application/json");
|
|
20
|
+
res.end(JSON.stringify(body));
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
if (req.method !== "POST")
|
|
24
|
+
return json(405, { error: "Method Not Allowed" });
|
|
25
|
+
if (!extractBearerToken(req))
|
|
26
|
+
return json(401, { error: "Unauthorized: bearer token mismatch" });
|
|
27
|
+
if (!approvalId.trim())
|
|
28
|
+
return json(400, { error: "Missing approvalId" });
|
|
29
|
+
const body = await readJsonBody(req);
|
|
30
|
+
if (!body)
|
|
31
|
+
return json(400, { error: "Invalid JSON body" });
|
|
32
|
+
const decision = typeof body.decision === "string" ? body.decision.trim() : "";
|
|
33
|
+
if (!VALID_DECISIONS.has(decision)) {
|
|
34
|
+
return json(400, { error: "decision must be allow-once | allow-always | deny" });
|
|
35
|
+
}
|
|
36
|
+
const deviceId = typeof body.deviceId === "string" ? body.deviceId.trim().toUpperCase() : "";
|
|
37
|
+
const cfg = getHostOpenClawConfigSnapshot(getFridayNextRuntime().config);
|
|
38
|
+
try {
|
|
39
|
+
await resolveApprovalOverGateway({
|
|
40
|
+
cfg: cfg,
|
|
41
|
+
approvalId: approvalId.trim(),
|
|
42
|
+
decision: decision,
|
|
43
|
+
senderId: deviceId || null,
|
|
44
|
+
allowPluginFallback: true,
|
|
45
|
+
clientDisplayName: deviceId ? `Friday Next (${deviceId})` : "Friday Next",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
log.error(`resolveApprovalOverGateway failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
return json(502, { error: "Approval resolution failed", detail: String(err) });
|
|
51
|
+
}
|
|
52
|
+
log.info(`approval ${approvalId} resolved decision=${decision} device=${deviceId || "(none)"}`);
|
|
53
|
+
return json(200, { ok: true, approvalId: approvalId.trim(), decision });
|
|
54
|
+
}
|
|
@@ -17,7 +17,7 @@ import { resolveAgentDefaults, setSessionSettings, splitModelRef, toSessionStore
|
|
|
17
17
|
import { sseEmitter } from "../../sse/emitter.js";
|
|
18
18
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
19
19
|
import { readJsonBody } from "../middleware/body.js";
|
|
20
|
-
import { registerFridaySessionDeviceMapping } from "../../friday-session.js";
|
|
20
|
+
import { forwardAgentEventRaw, isCodexRun, registerFridaySessionDeviceMapping, } from "../../friday-session.js";
|
|
21
21
|
import { touchFridayInbound } from "../../friday-inbound-stats.js";
|
|
22
22
|
import { fridayAttachmentLookupKey, fridayFilesPublicUrl, readFile, rememberInboundMediaName, resolveMediaAttachment, resolveMediaUrl, } from "./files.js";
|
|
23
23
|
import { runFridayDispatch } from "../../agent/dispatch-bridge.js";
|
|
@@ -467,6 +467,12 @@ export async function handleMessages(req, res) {
|
|
|
467
467
|
runId,
|
|
468
468
|
suppressTyping: true,
|
|
469
469
|
disableBlockStreaming: true,
|
|
470
|
+
// A1: feed the chosen thinking level into the run as a one-shot override so the model
|
|
471
|
+
// request asks for a reasoning summary. The session-stored `thinkingLevel` alone is NOT
|
|
472
|
+
// honored by the reply dispatch; `thinkingLevelOverride` has top priority in OpenClaw's
|
|
473
|
+
// resolution chain (get-reply-directives). Required for Codex (openai-chatgpt-responses)
|
|
474
|
+
// to emit any reasoning at all.
|
|
475
|
+
...(thinkingLevel ? { thinkingLevelOverride: thinkingLevel } : {}),
|
|
470
476
|
onModelSelected: (sel) => {
|
|
471
477
|
const name = typeof sel.model === "string" ? sel.model.trim() : "";
|
|
472
478
|
if (name) {
|
|
@@ -481,6 +487,18 @@ export async function handleMessages(req, res) {
|
|
|
481
487
|
: undefined;
|
|
482
488
|
const text = typeof rawText === "string" ? rawText : "";
|
|
483
489
|
log("REASONING_STREAM", normalizedDeviceId, runId, `textLen=${text.length}`);
|
|
490
|
+
// A2: the embedded runner already emits `stream: "thinking"` on the agent-event bus, so
|
|
491
|
+
// forwarding here would double it. The Codex app-server backend does NOT — reasoning text
|
|
492
|
+
// only arrives via this callback (cumulative snapshot). Forward it as a thinking event
|
|
493
|
+
// (reusing forwardAgentEventRaw's cumulative→delta rewrite) ONLY for Codex runs.
|
|
494
|
+
if (text && isCodexRun(runId)) {
|
|
495
|
+
forwardAgentEventRaw({
|
|
496
|
+
runId,
|
|
497
|
+
stream: "thinking",
|
|
498
|
+
data: { text },
|
|
499
|
+
sessionKey: baseSessionKey,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
484
502
|
},
|
|
485
503
|
onReasoningEnd: async () => {
|
|
486
504
|
log("REASONING_STREAM_END", normalizedDeviceId, runId);
|
package/dist/src/http/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { handleFilesDownload } from "./handlers/files-download.js";
|
|
|
11
11
|
import { handleCancel } from "./handlers/cancel.js";
|
|
12
12
|
import { handleDeviceApprove } from "./handlers/device-approve.js";
|
|
13
13
|
import { handleNodesApprove } from "./handlers/nodes-approve.js";
|
|
14
|
+
import { handleApprovalDecision } from "./handlers/approvals.js";
|
|
14
15
|
import { handleSessionsSettings } from "./handlers/sessions-settings.js";
|
|
15
16
|
import { handleModelsList } from "./handlers/models-list.js";
|
|
16
17
|
import { handleAgentsList } from "./handlers/agents-list.js";
|
|
@@ -65,6 +66,11 @@ async function handleFridayNextRoute(req, res) {
|
|
|
65
66
|
if (req.method === "POST" && pathname === "/friday-next/nodes-approve") {
|
|
66
67
|
return await handleNodesApprove(req, res);
|
|
67
68
|
}
|
|
69
|
+
// Route: POST /friday-next/approvals/{approvalId} (submit exec/plugin approval decision)
|
|
70
|
+
if (req.method === "POST" && pathname.startsWith("/friday-next/approvals/")) {
|
|
71
|
+
const approvalId = decodeURIComponent(pathname.slice("/friday-next/approvals/".length));
|
|
72
|
+
return await handleApprovalDecision(req, res, approvalId);
|
|
73
|
+
}
|
|
68
74
|
if ((req.method === "PUT" || req.method === "GET") &&
|
|
69
75
|
pathname === "/friday-next/sessions/settings") {
|
|
70
76
|
return await handleSessionsSettings(req, res);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
+
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage } from "node:http";
|
|
8
|
+
/**
|
|
9
|
+
* Extract and validate bearer token from Authorization header.
|
|
10
|
+
* Returns the token only if it matches the gateway's configured auth token.
|
|
11
|
+
* Returns null if token is missing, malformed, or doesn't match.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractBearerToken(req: IncomingMessage): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
+
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveFridayNextConfig } from "../../config.js";
|
|
8
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
9
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
10
|
+
/**
|
|
11
|
+
* Extract and validate bearer token from Authorization header.
|
|
12
|
+
* Returns the token only if it matches the gateway's configured auth token.
|
|
13
|
+
* Returns null if token is missing, malformed, or doesn't match.
|
|
14
|
+
*/
|
|
15
|
+
export function extractBearerToken(req) {
|
|
16
|
+
const auth = req.headers.authorization;
|
|
17
|
+
if (!auth || typeof auth !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
const parts = auth.trim().split(/\s+/);
|
|
20
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer")
|
|
21
|
+
return null;
|
|
22
|
+
const token = parts[1];
|
|
23
|
+
// Validate token matches the gateway's configured auth token.
|
|
24
|
+
const cfg = getHostOpenClawConfigSnapshot(getFridayNextRuntime().config);
|
|
25
|
+
const runtimeConfig = resolveFridayNextConfig(cfg);
|
|
26
|
+
if (!runtimeConfig.authToken || token !== runtimeConfig.authToken)
|
|
27
|
+
return null;
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
2
|
+
return await new Promise((resolve) => {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let total = 0;
|
|
5
|
+
req.on("data", (chunk) => {
|
|
6
|
+
total += chunk.length;
|
|
7
|
+
if (total > maxBytes) {
|
|
8
|
+
resolve(null);
|
|
9
|
+
req.destroy();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
chunks.push(chunk);
|
|
13
|
+
});
|
|
14
|
+
req.on("end", () => {
|
|
15
|
+
try {
|
|
16
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
resolve(null);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
req.on("error", () => resolve(null));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { resolveFridayNextConfig } from "../../config.js";
|
|
2
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
3
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
4
|
+
export function applyCorsHeaders(res) {
|
|
5
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
6
|
+
if (!cfg.corsEnabled)
|
|
7
|
+
return;
|
|
8
|
+
res.setHeader("Access-Control-Allow-Origin", cfg.corsAllowOrigin || "*");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
|
|
10
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
11
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
|
-
export type SseEventType = "connected" | "agent" | "deliver" | "tool-hook" | "outbound" | "ping" | "subagent";
|
|
2
|
+
export type SseEventType = "connected" | "agent" | "deliver" | "tool-hook" | "outbound" | "ping" | "subagent" | "approval";
|
|
3
3
|
export interface SseEvent {
|
|
4
4
|
type: SseEventType;
|
|
5
5
|
data: Record<string, unknown>;
|
package/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { sseEmitter } from "./src/sse/emitter.js";
|
|
|
15
15
|
import {
|
|
16
16
|
forwardAgentEventRaw,
|
|
17
17
|
getLastRegisteredFridayDeviceId,
|
|
18
|
+
isCodexRun,
|
|
18
19
|
resolveFridayDeviceIdForSessionKey,
|
|
19
20
|
} from "./src/friday-session.js";
|
|
20
21
|
import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
|
|
@@ -22,6 +23,7 @@ import { setUpgradeRuntime } from "./src/upgrade-runtime.js";
|
|
|
22
23
|
import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
|
|
23
24
|
import { accumulateRunUsage } from "./src/agent/run-usage-accumulator.js";
|
|
24
25
|
import { createFridayNextLogger } from "./src/logging.js";
|
|
26
|
+
import { ensureCodexReasoningSummary } from "./src/codex-reasoning-config.js";
|
|
25
27
|
|
|
26
28
|
const hookLogger = createFridayNextLogger("hook");
|
|
27
29
|
|
|
@@ -63,6 +65,38 @@ function isFridaySessionKey(sk: string): boolean {
|
|
|
63
65
|
return /^friday-next-/i.test(sk) || /^agent:main:friday-next-/i.test(sk);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/** Shell/exec-style tools whose stdout the app renders as a `command_output` row (A3). */
|
|
69
|
+
const COMMAND_TOOL_NAMES = new Set([
|
|
70
|
+
"exec",
|
|
71
|
+
"bash",
|
|
72
|
+
"shell",
|
|
73
|
+
"local_shell",
|
|
74
|
+
"command",
|
|
75
|
+
"process",
|
|
76
|
+
"run_terminal_cmd",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
function isCommandTool(toolName: unknown): boolean {
|
|
80
|
+
return typeof toolName === "string" && COMMAND_TOOL_NAMES.has(toolName.trim().toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Best-effort flatten of an after-hook tool result into the stdout string the app expects. */
|
|
84
|
+
function coerceCommandOutput(result: unknown): string {
|
|
85
|
+
if (typeof result === "string") return result;
|
|
86
|
+
if (result && typeof result === "object") {
|
|
87
|
+
const r = result as Record<string, unknown>;
|
|
88
|
+
for (const key of ["output", "stdout", "text"]) {
|
|
89
|
+
if (typeof r[key] === "string") return r[key] as string;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(result);
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result == null ? "" : String(result);
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
function shouldForwardToolEventToFriday(ctx: PluginHookToolContext): boolean {
|
|
67
101
|
if (ctx.runId) {
|
|
68
102
|
if (sseEmitter.getDeviceIdByRunId(ctx.runId)) return true;
|
|
@@ -134,6 +168,10 @@ export default defineChannelPluginEntry({
|
|
|
134
168
|
}
|
|
135
169
|
fridayNextToolHooksRegistered = true;
|
|
136
170
|
|
|
171
|
+
// Make Codex (ChatGPT/OAuth) models emit reasoning summary text so the app can stream
|
|
172
|
+
// "thinking". OpenClaw never sets this; we assert it on the plugin side. Best-effort.
|
|
173
|
+
ensureCodexReasoningSummary((msg) => hookLogger.info(msg));
|
|
174
|
+
|
|
137
175
|
api.on("subagent_delivery_target", (event: any) => {
|
|
138
176
|
if (!event.expectsCompletionMessage) return;
|
|
139
177
|
const ch = event.requesterOrigin?.channel?.trim().toLowerCase();
|
|
@@ -219,6 +257,29 @@ export default defineChannelPluginEntry({
|
|
|
219
257
|
ts: Date.now(),
|
|
220
258
|
},
|
|
221
259
|
});
|
|
260
|
+
|
|
261
|
+
// A3: the Codex app-server backend never puts exec stdout on the `command_output` stream
|
|
262
|
+
// (its tool result carries only exitCode/duration), so the app shows the command row with no
|
|
263
|
+
// output. The after-hook DOES carry the full stdout — synthesize a `command_output` end event
|
|
264
|
+
// keyed by toolCallId (== the forwarded `item kind:command` itemId) so the app attaches it.
|
|
265
|
+
// Codex-only: embedded runs already stream `command_output` on the bus.
|
|
266
|
+
if (isCodexRun(runId) && event.toolCallId && isCommandTool(event.toolName)) {
|
|
267
|
+
const output = coerceCommandOutput(event.result);
|
|
268
|
+
if (output) {
|
|
269
|
+
forwardAgentEventRaw({
|
|
270
|
+
runId,
|
|
271
|
+
stream: "command_output",
|
|
272
|
+
data: {
|
|
273
|
+
itemId: event.toolCallId,
|
|
274
|
+
phase: "end",
|
|
275
|
+
output,
|
|
276
|
+
status: event.error ? "failed" : "completed",
|
|
277
|
+
durationMs: event.durationMs ?? null,
|
|
278
|
+
},
|
|
279
|
+
sessionKey: ctx.sessionKey,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
222
283
|
});
|
|
223
284
|
},
|
|
224
285
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syengup/friday-channel-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
"tsconfig.json",
|
|
13
13
|
"openclaw.plugin.json"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"format": "prettier --write .",
|
|
20
|
+
"format:check": "prettier --check .",
|
|
21
|
+
"prepublishOnly": "pnpm build && rm -rf dist/attachments",
|
|
22
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
23
|
+
"test:unit": "vitest run",
|
|
24
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
25
|
+
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
26
|
+
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
27
|
+
},
|
|
15
28
|
"bin": {
|
|
16
29
|
"friday-channel-next": "install.js"
|
|
17
30
|
},
|
|
@@ -62,17 +75,5 @@
|
|
|
62
75
|
"typescript-eslint": "^8.61.1",
|
|
63
76
|
"vitest": "^4.1.5",
|
|
64
77
|
"zod": "^4.3.6"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"build": "tsc -p tsconfig.json",
|
|
68
|
-
"lint": "eslint .",
|
|
69
|
-
"lint:fix": "eslint . --fix",
|
|
70
|
-
"format": "prettier --write .",
|
|
71
|
-
"format:check": "prettier --check .",
|
|
72
|
-
"test": "npm run test:unit && npm run test:e2e",
|
|
73
|
-
"test:unit": "vitest run",
|
|
74
|
-
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
75
|
-
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
76
|
-
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
77
78
|
}
|
|
78
|
-
}
|
|
79
|
+
}
|
|
@@ -48,7 +48,9 @@ export function registerSessionKeyForRun(sessionKey: string, runId: string): voi
|
|
|
48
48
|
*/
|
|
49
49
|
const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
|
|
50
50
|
|
|
51
|
-
function parseAnnounceRunId(
|
|
51
|
+
export function parseAnnounceRunId(
|
|
52
|
+
runId: string,
|
|
53
|
+
): { childSessionKey: string; bareRunId: string } | null {
|
|
52
54
|
const m = runId.match(ANNOUNCE_RUN_ID_RE);
|
|
53
55
|
if (!m) return null;
|
|
54
56
|
return { childSessionKey: m[1] ?? "", bareRunId: m[2] ?? "" };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildPayload } from "./friday-approval-capability.js";
|
|
3
|
+
|
|
4
|
+
const execView = {
|
|
5
|
+
approvalId: "exec:abc",
|
|
6
|
+
approvalKind: "exec",
|
|
7
|
+
title: "Codex command approval",
|
|
8
|
+
commandText: "curl -sS https://example.com",
|
|
9
|
+
commandPreview: "curl ...",
|
|
10
|
+
cwd: "/ws",
|
|
11
|
+
host: "sandbox",
|
|
12
|
+
metadata: [{ label: "Severity", value: "Warning" }],
|
|
13
|
+
actions: [
|
|
14
|
+
{ decision: "allow-once", label: "Allow Once", style: "primary" },
|
|
15
|
+
{ decision: "deny", label: "Deny", style: "danger" },
|
|
16
|
+
],
|
|
17
|
+
expiresAtMs: 123,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pluginView = {
|
|
21
|
+
approvalId: "plugin:xyz",
|
|
22
|
+
approvalKind: "plugin",
|
|
23
|
+
title: "Codex app-server command approval",
|
|
24
|
+
description: "Command: curl ...",
|
|
25
|
+
toolName: "codex_command_approval",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
metadata: [{ label: "Tool", value: "codex_command_approval" }],
|
|
28
|
+
actions: [{ decision: "allow-always", label: "Allow Always", style: "secondary" }],
|
|
29
|
+
expiresAtMs: 456,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const reqWith = (sessionKey: string) => ({ request: { sessionKey } });
|
|
33
|
+
|
|
34
|
+
describe("buildPayload", () => {
|
|
35
|
+
it("maps an exec approval view (command/cwd/host + actions)", () => {
|
|
36
|
+
const p = buildPayload({
|
|
37
|
+
op: "request",
|
|
38
|
+
view: execView,
|
|
39
|
+
request: reqWith("agent:main:fridaynext:s1"),
|
|
40
|
+
deviceId: "DEV1",
|
|
41
|
+
});
|
|
42
|
+
expect(p.op).toBe("request");
|
|
43
|
+
expect(p.kind).toBe("exec");
|
|
44
|
+
expect(p.approvalId).toBe("exec:abc");
|
|
45
|
+
expect(p.commandText).toBe("curl -sS https://example.com");
|
|
46
|
+
expect(p.cwd).toBe("/ws");
|
|
47
|
+
expect(p.host).toBe("sandbox");
|
|
48
|
+
expect(p.actions.map((a) => a.decision)).toEqual(["allow-once", "deny"]);
|
|
49
|
+
expect(p.sessionKey).toBe("agent:main:fridaynext:s1");
|
|
50
|
+
expect(p.deviceId).toBe("DEV1");
|
|
51
|
+
expect(p.expiresAtMs).toBe(123);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("maps a plugin approval view (toolName/severity/description)", () => {
|
|
55
|
+
const p = buildPayload({
|
|
56
|
+
op: "request",
|
|
57
|
+
view: pluginView,
|
|
58
|
+
request: reqWith("agent:main:fridaynext:s2"),
|
|
59
|
+
deviceId: "DEV2",
|
|
60
|
+
});
|
|
61
|
+
expect(p.kind).toBe("plugin");
|
|
62
|
+
expect(p.toolName).toBe("codex_command_approval");
|
|
63
|
+
expect(p.severity).toBe("warning");
|
|
64
|
+
expect(p.description).toBe("Command: curl ...");
|
|
65
|
+
expect(p.actions[0].decision).toBe("allow-always");
|
|
66
|
+
expect(p.commandText).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("defaults missing fields without throwing", () => {
|
|
70
|
+
const p = buildPayload({ op: "expired", view: {}, request: {}, deviceId: "D" });
|
|
71
|
+
expect(p.op).toBe("expired");
|
|
72
|
+
expect(p.kind).toBe("exec");
|
|
73
|
+
expect(p.approvalId).toBe("");
|
|
74
|
+
expect(p.actions).toEqual([]);
|
|
75
|
+
expect(p.metadata).toEqual([]);
|
|
76
|
+
expect(p.sessionKey).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|