@holon-run/agentinbox 0.1.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/LICENSE +178 -0
- package/README.md +168 -0
- package/dist/src/adapters.js +111 -0
- package/dist/src/backend.js +175 -0
- package/dist/src/cli.js +620 -0
- package/dist/src/client.js +279 -0
- package/dist/src/control_server.js +93 -0
- package/dist/src/daemon.js +246 -0
- package/dist/src/filter.js +167 -0
- package/dist/src/http.js +408 -0
- package/dist/src/matcher.js +47 -0
- package/dist/src/model.js +2 -0
- package/dist/src/paths.js +141 -0
- package/dist/src/service.js +1338 -0
- package/dist/src/source_schema.js +150 -0
- package/dist/src/sources/feishu.js +567 -0
- package/dist/src/sources/github.js +485 -0
- package/dist/src/sources/github_ci.js +372 -0
- package/dist/src/store.js +1271 -0
- package/dist/src/terminal.js +301 -0
- package/dist/src/util.js +36 -0
- package/package.json +52 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchSubscriptionFilter = matchSubscriptionFilter;
|
|
4
|
+
exports.validateSubscriptionFilter = validateSubscriptionFilter;
|
|
5
|
+
const { Jexl } = require("jexl");
|
|
6
|
+
const jexl = new Jexl();
|
|
7
|
+
const MAX_REGEX_PATTERN_LENGTH = 256;
|
|
8
|
+
const MAX_REGEX_VALUE_LENGTH = 4_096;
|
|
9
|
+
jexl.addFunction("contains", (search, item) => {
|
|
10
|
+
if (typeof search === "string") {
|
|
11
|
+
return typeof item === "string" ? search.includes(item) : false;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(search)) {
|
|
14
|
+
return search.some((value) => deepEqual(value, item));
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
});
|
|
18
|
+
jexl.addFunction("startsWith", (value, search) => (typeof value === "string" && typeof search === "string" ? value.startsWith(search) : false));
|
|
19
|
+
jexl.addFunction("endsWith", (value, search) => (typeof value === "string" && typeof search === "string" ? value.endsWith(search) : false));
|
|
20
|
+
jexl.addFunction("format", (template, ...values) => {
|
|
21
|
+
if (typeof template !== "string") {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return template.replace(/\{(\d+)\}/g, (_match, index) => String(values[Number(index)] ?? ""));
|
|
25
|
+
});
|
|
26
|
+
jexl.addFunction("join", (value, separator) => (Array.isArray(value) ? value.join(typeof separator === "string" ? separator : ",") : ""));
|
|
27
|
+
jexl.addFunction("matches", (value, pattern) => {
|
|
28
|
+
if (typeof value !== "string" || typeof pattern !== "string") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (value.length > MAX_REGEX_VALUE_LENGTH) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const regex = compileSafeRegex(pattern);
|
|
36
|
+
return regex ? regex.test(value) : false;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
jexl.addFunction("glob", (value, pattern) => (typeof value === "string" && typeof pattern === "string" ? globMatch(value, pattern) : false));
|
|
43
|
+
jexl.addFunction("exists", (value) => value !== undefined && value !== null);
|
|
44
|
+
async function matchSubscriptionFilter(filter, context) {
|
|
45
|
+
if (filter.metadata && !matchesShortcutFilter(filter.metadata, context.metadata)) {
|
|
46
|
+
return { matched: false, reason: "metadata shortcut mismatch" };
|
|
47
|
+
}
|
|
48
|
+
if (filter.payload && !matchesShortcutFilter(filter.payload, context.payload)) {
|
|
49
|
+
return { matched: false, reason: "payload shortcut mismatch" };
|
|
50
|
+
}
|
|
51
|
+
if (filter.expr) {
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await jexl.eval(filter.expr, context);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return { matched: false, reason: `expr evaluation failed: ${message}` };
|
|
59
|
+
}
|
|
60
|
+
if (!Boolean(result)) {
|
|
61
|
+
return { matched: false, reason: "expr returned false" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { matched: true, reason: filter.expr ? "filter shortcuts and expr matched" : "filter shortcuts matched" };
|
|
65
|
+
}
|
|
66
|
+
async function validateSubscriptionFilter(filter) {
|
|
67
|
+
if (!filter.expr) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await jexl.eval(filter.expr, {
|
|
71
|
+
metadata: {},
|
|
72
|
+
payload: {},
|
|
73
|
+
eventVariant: "",
|
|
74
|
+
sourceType: "",
|
|
75
|
+
sourceKey: "",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function matchesShortcutFilter(expected, actual) {
|
|
79
|
+
return Object.entries(expected).every(([key, value]) => matchesExpectedValue(readPath(actual, key), value));
|
|
80
|
+
}
|
|
81
|
+
function matchesExpectedValue(actual, expected) {
|
|
82
|
+
if (isPlainObject(expected)) {
|
|
83
|
+
if (!isPlainObject(actual)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return Object.entries(expected).every(([key, value]) => matchesExpectedValue(readPath(actual, key), value));
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(expected)) {
|
|
89
|
+
if (Array.isArray(actual)) {
|
|
90
|
+
return expected.every((expectedValue) => actual.some((actualValue) => matchesExpectedValue(actualValue, expectedValue)));
|
|
91
|
+
}
|
|
92
|
+
return expected.some((expectedValue) => matchesExpectedValue(actual, expectedValue));
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(actual)) {
|
|
95
|
+
return actual.some((actualValue) => matchesExpectedValue(actualValue, expected));
|
|
96
|
+
}
|
|
97
|
+
return deepEqual(actual, expected);
|
|
98
|
+
}
|
|
99
|
+
function readPath(value, path) {
|
|
100
|
+
if (!path.includes(".")) {
|
|
101
|
+
if (!isPlainObject(value)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return value[path];
|
|
105
|
+
}
|
|
106
|
+
return path.split(".").reduce((current, segment) => {
|
|
107
|
+
if (!isPlainObject(current)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return current[segment];
|
|
111
|
+
}, value);
|
|
112
|
+
}
|
|
113
|
+
function isPlainObject(value) {
|
|
114
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
function deepEqual(left, right) {
|
|
117
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
118
|
+
return left.length === right.length && left.every((value, index) => deepEqual(value, right[index]));
|
|
119
|
+
}
|
|
120
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
121
|
+
const leftKeys = Object.keys(left);
|
|
122
|
+
const rightKeys = Object.keys(right);
|
|
123
|
+
return leftKeys.length === rightKeys.length
|
|
124
|
+
&& leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
125
|
+
}
|
|
126
|
+
return left === right;
|
|
127
|
+
}
|
|
128
|
+
function globMatch(value, pattern) {
|
|
129
|
+
const regex = new RegExp(`^${globToRegex(pattern)}$`);
|
|
130
|
+
return regex.test(value);
|
|
131
|
+
}
|
|
132
|
+
function compileSafeRegex(pattern) {
|
|
133
|
+
if (pattern.length === 0 || pattern.length > MAX_REGEX_PATTERN_LENGTH) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (pattern.includes("(?") || /\\[1-9]/.test(pattern)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (hasNestedQuantifier(pattern)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return new RegExp(pattern);
|
|
143
|
+
}
|
|
144
|
+
function hasNestedQuantifier(pattern) {
|
|
145
|
+
return /\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)[+*{]/.test(pattern);
|
|
146
|
+
}
|
|
147
|
+
function globToRegex(pattern) {
|
|
148
|
+
let regex = "";
|
|
149
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
150
|
+
const char = pattern[index];
|
|
151
|
+
const next = pattern[index + 1];
|
|
152
|
+
if (char === "*" && next === "*") {
|
|
153
|
+
regex += ".*";
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (char === "*") {
|
|
158
|
+
regex += "[^/]*";
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
regex += escapeRegex(char);
|
|
162
|
+
}
|
|
163
|
+
return regex;
|
|
164
|
+
}
|
|
165
|
+
function escapeRegex(value) {
|
|
166
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
167
|
+
}
|
package/dist/src/http.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createServer = createServer;
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
+
const util_1 = require("./util");
|
|
9
|
+
async function readJson(req) {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for await (const chunk of req) {
|
|
12
|
+
chunks.push(Buffer.from(chunk));
|
|
13
|
+
}
|
|
14
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
15
|
+
if (!body) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
return (0, util_1.asObject)(JSON.parse(body));
|
|
19
|
+
}
|
|
20
|
+
function send(res, statusCode, data) {
|
|
21
|
+
res.statusCode = statusCode;
|
|
22
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
23
|
+
res.end((0, util_1.jsonResponse)(data));
|
|
24
|
+
}
|
|
25
|
+
function sendSse(res, event, data) {
|
|
26
|
+
res.write(`event: ${event}\n`);
|
|
27
|
+
const payload = (0, util_1.jsonResponse)(data)
|
|
28
|
+
.split("\n")
|
|
29
|
+
.map((line) => `data: ${line}`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
res.write(`${payload}\n\n`);
|
|
32
|
+
}
|
|
33
|
+
function createServer(service) {
|
|
34
|
+
return node_http_1.default.createServer(async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
if (!req.url || !req.method) {
|
|
37
|
+
send(res, 400, { error: "missing request metadata" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
41
|
+
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
42
|
+
send(res, 200, { ok: true });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (req.method === "GET" && url.pathname === "/status") {
|
|
46
|
+
send(res, 200, service.status());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (req.method === "POST" && url.pathname === "/gc") {
|
|
50
|
+
send(res, 200, service.gc());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (req.method === "GET" && url.pathname === "/sources") {
|
|
54
|
+
send(res, 200, { sources: service.listSources() });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (req.method === "POST" && url.pathname === "/sources/register") {
|
|
58
|
+
send(res, 200, await service.registerSource(await readJson(req)));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const sourceMatch = url.pathname.match(/^\/sources\/([^/]+)$/);
|
|
62
|
+
if (req.method === "GET" && sourceMatch) {
|
|
63
|
+
send(res, 200, service.getSourceDetails(decodeURIComponent(sourceMatch[1])));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const sourceSchemaMatch = url.pathname.match(/^\/source-types\/([^/]+)\/schema$/);
|
|
67
|
+
if (req.method === "GET" && sourceSchemaMatch) {
|
|
68
|
+
send(res, 200, service.getSourceSchema(decodeURIComponent(sourceSchemaMatch[1])));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const sourcePollMatch = url.pathname.match(/^\/sources\/([^/]+)\/poll$/);
|
|
72
|
+
if (req.method === "POST" && sourcePollMatch) {
|
|
73
|
+
send(res, 200, await service.pollSource(decodeURIComponent(sourcePollMatch[1])));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const sourceEventsMatch = url.pathname.match(/^\/sources\/([^/]+)\/events$/);
|
|
77
|
+
if (req.method === "POST" && sourceEventsMatch) {
|
|
78
|
+
const body = await readJson(req);
|
|
79
|
+
const sourceId = decodeURIComponent(sourceEventsMatch[1]);
|
|
80
|
+
const sourceNativeId = parseRequiredString(body.sourceNativeId, "sources/events requires sourceNativeId");
|
|
81
|
+
const eventVariant = parseRequiredString(body.eventVariant, "sources/events requires eventVariant");
|
|
82
|
+
send(res, 200, await service.appendSourceEventByCaller(sourceId, {
|
|
83
|
+
sourceNativeId,
|
|
84
|
+
eventVariant,
|
|
85
|
+
occurredAt: parseOptionalString(body.occurredAt),
|
|
86
|
+
metadata: (0, util_1.asObject)(body.metadata),
|
|
87
|
+
rawPayload: (0, util_1.asObject)(body.rawPayload),
|
|
88
|
+
deliveryHandle: parseOptionalDeliveryHandle(body.deliveryHandle),
|
|
89
|
+
}));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (req.method === "GET" && url.pathname === "/agents") {
|
|
93
|
+
send(res, 200, { agents: service.listAgents() });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (req.method === "POST" && url.pathname === "/agents/register") {
|
|
97
|
+
const body = await readJson(req);
|
|
98
|
+
const backend = parseRequiredString(body.backend, "agents/register requires backend");
|
|
99
|
+
send(res, 200, service.registerAgent({
|
|
100
|
+
agentId: parseOptionalString(body.agentId) ?? null,
|
|
101
|
+
forceRebind: parseOptionalBoolean(body.forceRebind) ?? false,
|
|
102
|
+
backend: backend,
|
|
103
|
+
runtimeKind: parseOptionalString(body.runtimeKind),
|
|
104
|
+
runtimeSessionId: parseOptionalString(body.runtimeSessionId),
|
|
105
|
+
mode: "agent_prompt",
|
|
106
|
+
tmuxPaneId: parseOptionalString(body.tmuxPaneId),
|
|
107
|
+
tty: parseOptionalString(body.tty),
|
|
108
|
+
termProgram: parseOptionalString(body.termProgram),
|
|
109
|
+
itermSessionId: parseOptionalString(body.itermSessionId),
|
|
110
|
+
notifyLeaseMs: parseOptionalInteger(body.notifyLeaseMs) ?? null,
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const agentMatch = url.pathname.match(/^\/agents\/([^/]+)$/);
|
|
115
|
+
if (req.method === "GET" && agentMatch) {
|
|
116
|
+
send(res, 200, service.getAgentDetails(decodeURIComponent(agentMatch[1])));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (req.method === "DELETE" && agentMatch) {
|
|
120
|
+
send(res, 200, service.removeAgent(decodeURIComponent(agentMatch[1])));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const agentTargetsMatch = url.pathname.match(/^\/agents\/([^/]+)\/targets$/);
|
|
124
|
+
if (req.method === "GET" && agentTargetsMatch) {
|
|
125
|
+
send(res, 200, {
|
|
126
|
+
targets: service.listActivationTargets(decodeURIComponent(agentTargetsMatch[1])),
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (req.method === "POST" && agentTargetsMatch) {
|
|
131
|
+
const body = await readJson(req);
|
|
132
|
+
const agentId = decodeURIComponent(agentTargetsMatch[1]);
|
|
133
|
+
const kind = parseRequiredString(body.kind, "agents/targets requires kind");
|
|
134
|
+
if (kind !== "webhook") {
|
|
135
|
+
send(res, 400, { error: `unsupported activation target kind: ${kind}` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const urlValue = parseRequiredString(body.url, "agents/targets requires url");
|
|
139
|
+
send(res, 200, service.addWebhookActivationTarget(agentId, {
|
|
140
|
+
url: urlValue,
|
|
141
|
+
activationMode: parseOptionalString(body.activationMode),
|
|
142
|
+
notifyLeaseMs: parseOptionalInteger(body.notifyLeaseMs) ?? null,
|
|
143
|
+
}));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const agentTargetMatch = url.pathname.match(/^\/agents\/([^/]+)\/targets\/([^/]+)$/);
|
|
147
|
+
if (req.method === "DELETE" && agentTargetMatch) {
|
|
148
|
+
send(res, 200, service.removeActivationTarget(decodeURIComponent(agentTargetMatch[1]), decodeURIComponent(agentTargetMatch[2])));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (req.method === "GET" && url.pathname === "/subscriptions") {
|
|
152
|
+
send(res, 200, {
|
|
153
|
+
subscriptions: service.listSubscriptions({
|
|
154
|
+
sourceId: url.searchParams.get("source_id") ?? undefined,
|
|
155
|
+
agentId: url.searchParams.get("agent_id") ?? undefined,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (req.method === "POST" && url.pathname === "/subscriptions/register") {
|
|
161
|
+
const body = await readJson(req);
|
|
162
|
+
send(res, 200, await service.registerSubscription({
|
|
163
|
+
agentId: parseRequiredString(body.agentId, "subscriptions/register requires agentId"),
|
|
164
|
+
sourceId: parseRequiredString(body.sourceId, "subscriptions/register requires sourceId"),
|
|
165
|
+
filter: (0, util_1.asObject)(body.filter),
|
|
166
|
+
lifecycleMode: parseOptionalString(body.lifecycleMode),
|
|
167
|
+
expiresAt: parseOptionalString(body.expiresAt) ?? null,
|
|
168
|
+
startPolicy: parseOptionalString(body.startPolicy),
|
|
169
|
+
startOffset: parseOptionalInteger(body.startOffset),
|
|
170
|
+
startTime: parseOptionalString(body.startTime) ?? undefined,
|
|
171
|
+
}));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const subscriptionMatch = url.pathname.match(/^\/subscriptions\/([^/]+)$/);
|
|
175
|
+
if (req.method === "GET" && subscriptionMatch) {
|
|
176
|
+
send(res, 200, await service.getSubscriptionDetails(decodeURIComponent(subscriptionMatch[1])));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (req.method === "DELETE" && subscriptionMatch) {
|
|
180
|
+
send(res, 200, await service.removeSubscription(decodeURIComponent(subscriptionMatch[1])));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const subscriptionPollMatch = url.pathname.match(/^\/subscriptions\/([^/]+)\/poll$/);
|
|
184
|
+
if (req.method === "POST" && subscriptionPollMatch) {
|
|
185
|
+
send(res, 200, await service.pollSubscription(decodeURIComponent(subscriptionPollMatch[1])));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const subscriptionLagMatch = url.pathname.match(/^\/subscriptions\/([^/]+)\/lag$/);
|
|
189
|
+
if (req.method === "GET" && subscriptionLagMatch) {
|
|
190
|
+
send(res, 200, await service.getSubscriptionLag(decodeURIComponent(subscriptionLagMatch[1])));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const subscriptionResetMatch = url.pathname.match(/^\/subscriptions\/([^/]+)\/reset$/);
|
|
194
|
+
if (req.method === "POST" && subscriptionResetMatch) {
|
|
195
|
+
const body = await readJson(req);
|
|
196
|
+
const startPolicy = parseRequiredString(body.startPolicy, "subscriptions/reset requires startPolicy");
|
|
197
|
+
send(res, 200, await service.resetSubscription({
|
|
198
|
+
subscriptionId: decodeURIComponent(subscriptionResetMatch[1]),
|
|
199
|
+
startPolicy: startPolicy,
|
|
200
|
+
startOffset: parseOptionalInteger(body.startOffset),
|
|
201
|
+
startTime: parseOptionalString(body.startTime) ?? null,
|
|
202
|
+
}));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (req.method === "POST" && url.pathname === "/fixtures/emit") {
|
|
206
|
+
const body = await readJson(req);
|
|
207
|
+
const sourceId = parseRequiredString(body.sourceId, "fixtures/emit requires sourceId");
|
|
208
|
+
const sourceNativeId = parseRequiredString(body.sourceNativeId, "fixtures/emit requires sourceNativeId");
|
|
209
|
+
const eventVariant = parseRequiredString(body.eventVariant, "fixtures/emit requires eventVariant");
|
|
210
|
+
send(res, 200, await service.appendFixtureEvent(sourceId, {
|
|
211
|
+
sourceNativeId,
|
|
212
|
+
eventVariant,
|
|
213
|
+
occurredAt: parseOptionalString(body.occurredAt),
|
|
214
|
+
metadata: (0, util_1.asObject)(body.metadata),
|
|
215
|
+
rawPayload: (0, util_1.asObject)(body.rawPayload),
|
|
216
|
+
deliveryHandle: parseOptionalDeliveryHandle(body.deliveryHandle),
|
|
217
|
+
}));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const agentInboxMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox$/);
|
|
221
|
+
if (req.method === "GET" && agentInboxMatch) {
|
|
222
|
+
send(res, 200, service.getInboxDetailsByAgent(decodeURIComponent(agentInboxMatch[1])));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const agentInboxItemsMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox\/items$/);
|
|
226
|
+
if (req.method === "GET" && agentInboxItemsMatch) {
|
|
227
|
+
send(res, 200, {
|
|
228
|
+
items: service.listInboxItems(decodeURIComponent(agentInboxItemsMatch[1]), {
|
|
229
|
+
afterItemId: url.searchParams.get("after_item_id") ?? undefined,
|
|
230
|
+
includeAcked: url.searchParams.has("include_acked")
|
|
231
|
+
? url.searchParams.get("include_acked") === "true"
|
|
232
|
+
: undefined,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const agentInboxWatchMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox\/watch$/);
|
|
238
|
+
if (req.method === "GET" && agentInboxWatchMatch) {
|
|
239
|
+
const agentId = decodeURIComponent(agentInboxWatchMatch[1]);
|
|
240
|
+
const watchOptions = {
|
|
241
|
+
afterItemId: url.searchParams.get("after_item_id") ?? undefined,
|
|
242
|
+
includeAcked: url.searchParams.has("include_acked")
|
|
243
|
+
? url.searchParams.get("include_acked") === "true"
|
|
244
|
+
: undefined,
|
|
245
|
+
heartbeatMs: url.searchParams.has("heartbeat_ms")
|
|
246
|
+
? parsePositiveInteger(url.searchParams.get("heartbeat_ms"))
|
|
247
|
+
: undefined,
|
|
248
|
+
};
|
|
249
|
+
res.writeHead(200, {
|
|
250
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
251
|
+
"cache-control": "no-cache, no-transform",
|
|
252
|
+
connection: "keep-alive",
|
|
253
|
+
});
|
|
254
|
+
const session = service.watchInbox(agentId, watchOptions, (event) => {
|
|
255
|
+
sendSse(res, event.event, event);
|
|
256
|
+
});
|
|
257
|
+
sendSse(res, "items", {
|
|
258
|
+
event: "items",
|
|
259
|
+
agentId,
|
|
260
|
+
items: session.initialItems,
|
|
261
|
+
});
|
|
262
|
+
session.start();
|
|
263
|
+
const heartbeatMs = watchOptions.heartbeatMs ?? 15_000;
|
|
264
|
+
const heartbeat = setInterval(() => {
|
|
265
|
+
sendSse(res, "heartbeat", {
|
|
266
|
+
event: "heartbeat",
|
|
267
|
+
agentId,
|
|
268
|
+
timestamp: new Date().toISOString(),
|
|
269
|
+
});
|
|
270
|
+
}, heartbeatMs);
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
clearInterval(heartbeat);
|
|
273
|
+
session.close();
|
|
274
|
+
};
|
|
275
|
+
req.on("close", cleanup);
|
|
276
|
+
req.on("error", cleanup);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const agentInboxAckAllMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox\/ack-all$/);
|
|
280
|
+
if (req.method === "POST" && agentInboxAckAllMatch) {
|
|
281
|
+
send(res, 200, service.ackAllInboxItems(decodeURIComponent(agentInboxAckAllMatch[1])));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const agentInboxCompactMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox\/compact$/);
|
|
285
|
+
if (req.method === "POST" && agentInboxCompactMatch) {
|
|
286
|
+
send(res, 200, service.compactInbox(decodeURIComponent(agentInboxCompactMatch[1])));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const agentInboxAckMatch = url.pathname.match(/^\/agents\/([^/]+)\/inbox\/ack$/);
|
|
290
|
+
if (req.method === "POST" && agentInboxAckMatch) {
|
|
291
|
+
const body = await readJson(req);
|
|
292
|
+
const itemIds = Array.isArray(body.itemIds) ? body.itemIds.map((itemId) => String(itemId)) : [];
|
|
293
|
+
const throughItemId = parseOptionalString(body.throughItemId);
|
|
294
|
+
if (throughItemId && itemIds.length > 0) {
|
|
295
|
+
send(res, 400, { error: "inbox/ack accepts either itemIds or throughItemId" });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (throughItemId) {
|
|
299
|
+
send(res, 200, service.ackInboxItemsThrough(decodeURIComponent(agentInboxAckMatch[1]), throughItemId));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
send(res, 200, service.ackInboxItems(decodeURIComponent(agentInboxAckMatch[1]), itemIds));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (req.method === "POST" && url.pathname === "/deliveries/send") {
|
|
306
|
+
send(res, 200, await service.sendDelivery(await readJson(req)));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
send(res, 404, { error: "not found" });
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error instanceof SyntaxError) {
|
|
313
|
+
send(res, 400, { error: error.message });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
if (message.startsWith("unknown ")) {
|
|
318
|
+
send(res, 404, { error: message });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (isBadRequestError(message)) {
|
|
322
|
+
send(res, 400, { error: message });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
send(res, 500, { error: message });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function parseRequiredString(value, message) {
|
|
330
|
+
const parsed = parseOptionalString(value);
|
|
331
|
+
if (!parsed) {
|
|
332
|
+
throw new Error(message);
|
|
333
|
+
}
|
|
334
|
+
return parsed;
|
|
335
|
+
}
|
|
336
|
+
function parseOptionalString(value) {
|
|
337
|
+
if (typeof value !== "string") {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
const trimmed = value.trim();
|
|
341
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
342
|
+
}
|
|
343
|
+
function parseOptionalInteger(value) {
|
|
344
|
+
if (value == null) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const parsed = Number(value);
|
|
348
|
+
if (!Number.isInteger(parsed)) {
|
|
349
|
+
throw new Error(`expected integer, received ${String(value)}`);
|
|
350
|
+
}
|
|
351
|
+
return parsed;
|
|
352
|
+
}
|
|
353
|
+
function parseOptionalBoolean(value) {
|
|
354
|
+
if (value == null) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
if (typeof value !== "boolean") {
|
|
358
|
+
throw new Error(`expected boolean, received ${String(value)}`);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function parsePositiveInteger(value) {
|
|
363
|
+
const parsed = parseOptionalInteger(value);
|
|
364
|
+
if (parsed == null || parsed <= 0) {
|
|
365
|
+
throw new Error(`expected positive integer, received ${String(value)}`);
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
function isBadRequestError(message) {
|
|
370
|
+
return (message.startsWith("manual append is not supported") ||
|
|
371
|
+
message.startsWith("fixtures/emit requires") ||
|
|
372
|
+
message.startsWith("sources/events requires") ||
|
|
373
|
+
message.startsWith("deliveryHandle requires") ||
|
|
374
|
+
message.startsWith("subscriptions/reset requires") ||
|
|
375
|
+
message.startsWith("agents/register requires") ||
|
|
376
|
+
message.startsWith("agents/targets requires") ||
|
|
377
|
+
message.startsWith("agent register conflict") ||
|
|
378
|
+
message.startsWith("unsupported activation target kind") ||
|
|
379
|
+
message.startsWith("unsupported lifecycle mode") ||
|
|
380
|
+
message.startsWith("unsupported start policy") ||
|
|
381
|
+
message.startsWith("unsupported terminal") ||
|
|
382
|
+
message.startsWith("source type is reserved and not yet supported") ||
|
|
383
|
+
message.startsWith("expected boolean") ||
|
|
384
|
+
message.startsWith("expected integer") ||
|
|
385
|
+
message.startsWith("expected positive integer") ||
|
|
386
|
+
message.startsWith("notifyLeaseMs must be a positive integer") ||
|
|
387
|
+
message.startsWith("invalid webhook activation target") ||
|
|
388
|
+
message.includes("requires tmuxPaneId") ||
|
|
389
|
+
message.includes("requires iTerm2 session identity") ||
|
|
390
|
+
message.includes("requires a supported terminal context") ||
|
|
391
|
+
message.includes("belongs to agent"));
|
|
392
|
+
}
|
|
393
|
+
function parseOptionalDeliveryHandle(value) {
|
|
394
|
+
if (!value) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const object = (0, util_1.asObject)(value);
|
|
398
|
+
const provider = parseRequiredString(object.provider, "deliveryHandle.provider is required");
|
|
399
|
+
const surface = parseRequiredString(object.surface, "deliveryHandle.surface is required");
|
|
400
|
+
const targetRef = parseRequiredString(object.targetRef, "deliveryHandle.targetRef is required");
|
|
401
|
+
return {
|
|
402
|
+
provider,
|
|
403
|
+
surface,
|
|
404
|
+
targetRef,
|
|
405
|
+
threadRef: parseOptionalString(object.threadRef) ?? null,
|
|
406
|
+
replyMode: parseOptionalString(object.replyMode) ?? null,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchSubscription = matchSubscription;
|
|
4
|
+
function matchSubscription(subscription, metadata, payload) {
|
|
5
|
+
const source = { ...payload, ...metadata };
|
|
6
|
+
const { matchRules } = subscription;
|
|
7
|
+
for (const [key, expected] of Object.entries(matchRules)) {
|
|
8
|
+
const actual = source[key];
|
|
9
|
+
if (Array.isArray(expected)) {
|
|
10
|
+
if (Array.isArray(actual)) {
|
|
11
|
+
const everyMatched = expected.every((value) => actual.includes(value));
|
|
12
|
+
if (!everyMatched) {
|
|
13
|
+
return { matched: false, reason: `field ${key} missing expected values` };
|
|
14
|
+
}
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!expected.includes(actual)) {
|
|
18
|
+
return { matched: false, reason: `field ${key} not in allowed set` };
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (typeof expected === "string" && expected.startsWith("contains:")) {
|
|
23
|
+
const needle = expected.slice("contains:".length);
|
|
24
|
+
if (Array.isArray(actual)) {
|
|
25
|
+
const matched = actual.some((value) => typeof value === "string" && value.includes(needle));
|
|
26
|
+
if (!matched) {
|
|
27
|
+
return { matched: false, reason: `field ${key} missing contains:${needle}` };
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (typeof actual !== "string" || !actual.includes(needle)) {
|
|
32
|
+
return { matched: false, reason: `field ${key} missing contains:${needle}` };
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(actual)) {
|
|
37
|
+
if (!actual.includes(expected)) {
|
|
38
|
+
return { matched: false, reason: `field ${key} array missing value` };
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (actual !== expected) {
|
|
43
|
+
return { matched: false, reason: `field ${key} mismatch` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { matched: true, reason: "all rules matched" };
|
|
47
|
+
}
|