quadwork 1.5.1 → 1.5.3
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/bin/quadwork.js +13 -1
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +1 -1
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +1 -1
- package/out/__next._tree.txt +1 -1
- package/out/_next/static/chunks/{0-7v31f-nsgw-.js → 09sq17vme9g6p.js} +1 -1
- package/out/_next/static/chunks/{0m83k84.midd1.js → 0wreuebrwlg.2.js} +1 -1
- package/out/_not-found/__next._full.txt +1 -1
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +1 -1
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +1 -1
- package/out/app-shell/__next._full.txt +1 -1
- package/out/app-shell/__next._head.txt +1 -1
- package/out/app-shell/__next._index.txt +1 -1
- package/out/app-shell/__next._tree.txt +1 -1
- package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
- package/out/app-shell/__next.app-shell.txt +1 -1
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +1 -1
- package/out/index.html +1 -1
- package/out/index.txt +1 -1
- package/out/project/_/__next._full.txt +2 -2
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +1 -1
- package/out/project/_/__next._tree.txt +1 -1
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/memory/__next._full.txt +1 -1
- package/out/project/_/memory/__next._head.txt +1 -1
- package/out/project/_/memory/__next._index.txt +1 -1
- package/out/project/_/memory/__next._tree.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.txt +1 -1
- package/out/project/_/memory/__next.project.txt +1 -1
- package/out/project/_/memory.html +1 -1
- package/out/project/_/memory.txt +1 -1
- package/out/project/_/queue/__next._full.txt +1 -1
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +1 -1
- package/out/project/_/queue/__next._tree.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +1 -1
- package/out/project/_.html +1 -1
- package/out/project/_.txt +2 -2
- package/out/settings/__next._full.txt +1 -1
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +1 -1
- package/out/settings/__next._tree.txt +1 -1
- package/out/settings/__next.settings.__PAGE__.txt +1 -1
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +1 -1
- package/out/setup/__next._full.txt +1 -1
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +1 -1
- package/out/setup/__next._tree.txt +1 -1
- package/out/setup/__next.setup.__PAGE__.txt +1 -1
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +1 -1
- package/package.json +1 -1
- package/server/routes.chatWsSend.test.js +161 -0
- package/server/routes.js +250 -32
- package/server/routes.telegramBridge.test.js +145 -2
- package/templates/config.toml +8 -0
- /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → X4zdS6Y6HkLOaElNeHwnq}/_buildManifest.js +0 -0
- /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → X4zdS6Y6HkLOaElNeHwnq}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{wxXtT0v8ALxniu3OdJwt5 → X4zdS6Y6HkLOaElNeHwnq}/_ssgManifest.js +0 -0
package/out/setup.txt
CHANGED
|
@@ -11,7 +11,7 @@ d:I[11717,["/_next/static/chunks/04_t39bv8y9pe.js","/_next/static/chunks/0ox7p_s
|
|
|
11
11
|
f:I[92243,["/_next/static/chunks/04_t39bv8y9pe.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default",1]
|
|
12
12
|
:HL["/_next/static/chunks/0ccoe1hsu70ql.css","style"]
|
|
13
13
|
:HL["/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
|
14
|
-
0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/04_t39bv8y9pe.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/084lff9v4p_vh.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"
|
|
14
|
+
0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/04_t39bv8y9pe.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/084lff9v4p_vh.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0ccoe1hsu70ql.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"X4zdS6Y6HkLOaElNeHwnq"}
|
|
15
15
|
10:[]
|
|
16
16
|
a:"$W10"
|
|
17
17
|
c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
package/package.json
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// #236 / quadwork#236: sendViaWebSocket ack/body/error regression.
|
|
2
|
+
// Stands up a minimal fake AgentChattr ws server that mirrors app.py's
|
|
3
|
+
// `type:"message"` handler behavior (accept the frame, assign an id +
|
|
4
|
+
// timestamp, broadcast `{type:"message", data: msg}` back to the
|
|
5
|
+
// sender) and verifies:
|
|
6
|
+
//
|
|
7
|
+
// 1. Successful send resolves with {ok:true, message:{id,…}} — the
|
|
8
|
+
// echoed broadcast frame, not a fake {ok:true}.
|
|
9
|
+
// 2. Attachments survive the round trip.
|
|
10
|
+
// 3. History replay on connect does NOT satisfy the ack (the echo
|
|
11
|
+
// must come AFTER the send, not before).
|
|
12
|
+
// 4. A premature close without an echo rejects with an error (the
|
|
13
|
+
// old fire-and-forget path silently resolved).
|
|
14
|
+
// 5. Close code 4003 rejects with err.code === "EAGENTCHATTR_401"
|
|
15
|
+
// so the /api/chat handler can surface a proper 401.
|
|
16
|
+
//
|
|
17
|
+
// Run with: node server/routes.chatWsSend.test.js
|
|
18
|
+
|
|
19
|
+
const assert = require("node:assert/strict");
|
|
20
|
+
const http = require("node:http");
|
|
21
|
+
const { WebSocketServer } = require("ws");
|
|
22
|
+
const { sendViaWebSocket } = require("./routes");
|
|
23
|
+
|
|
24
|
+
function startFakeAc({ historyBeforeAck = [], rejectWithCode = null, dropBeforeAck = false } = {}) {
|
|
25
|
+
const server = http.createServer();
|
|
26
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
27
|
+
server.on("upgrade", (req, socket, head) => {
|
|
28
|
+
if (rejectWithCode === 4003) {
|
|
29
|
+
// Accept the upgrade, then immediately close with 4003 the way
|
|
30
|
+
// AC's /ws handler does on invalid token.
|
|
31
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
32
|
+
ws.close(4003, "forbidden: invalid session token");
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
37
|
+
// Simulate history replay on connect — these must NOT satisfy
|
|
38
|
+
// the ack, even if they match (sender,text,channel).
|
|
39
|
+
let historyMaxId = 0;
|
|
40
|
+
for (const msg of historyBeforeAck) {
|
|
41
|
+
ws.send(JSON.stringify({ type: "message", data: msg }));
|
|
42
|
+
if (typeof msg.id === "number" && msg.id > historyMaxId) historyMaxId = msg.id;
|
|
43
|
+
}
|
|
44
|
+
// Mirror AC: emit one `type:"status"` frame after history so the
|
|
45
|
+
// client knows the replay is done. See agentchattr/app.py
|
|
46
|
+
// `broadcast_status()` call in the /ws handler (line ~1082).
|
|
47
|
+
ws.send(JSON.stringify({ type: "status", data: { ready: true } }));
|
|
48
|
+
let nextId = Math.max(9000, historyMaxId + 1);
|
|
49
|
+
ws.on("message", (raw) => {
|
|
50
|
+
const frame = JSON.parse(raw.toString());
|
|
51
|
+
if (frame.type !== "message") return;
|
|
52
|
+
if (dropBeforeAck) {
|
|
53
|
+
ws.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Mirror store.add: assign id + timestamp, rebroadcast.
|
|
57
|
+
const echoed = {
|
|
58
|
+
id: nextId++,
|
|
59
|
+
sender: frame.sender,
|
|
60
|
+
text: frame.text,
|
|
61
|
+
channel: frame.channel || "general",
|
|
62
|
+
attachments: frame.attachments || [],
|
|
63
|
+
reply_to: frame.reply_to ?? null,
|
|
64
|
+
timestamp: Date.now() / 1000,
|
|
65
|
+
};
|
|
66
|
+
ws.send(JSON.stringify({ type: "message", data: echoed }));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
server.listen(0, "127.0.0.1", () => {
|
|
72
|
+
const { port } = server.address();
|
|
73
|
+
resolve({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
(async () => {
|
|
79
|
+
// 1 + 2) Success path: echo carries server id + attachments preserved.
|
|
80
|
+
{
|
|
81
|
+
const ac = await startFakeAc();
|
|
82
|
+
const result = await sendViaWebSocket(ac.url, "fake-token", {
|
|
83
|
+
text: "hello from test",
|
|
84
|
+
sender: "user",
|
|
85
|
+
channel: "general",
|
|
86
|
+
attachments: [{ url: "/uploads/x.png", name: "x.png" }],
|
|
87
|
+
});
|
|
88
|
+
assert.equal(result.ok, true);
|
|
89
|
+
assert.ok(result.message, "expected echoed message object");
|
|
90
|
+
assert.equal(typeof result.message.id, "number");
|
|
91
|
+
assert.ok(result.message.id >= 9000);
|
|
92
|
+
assert.equal(result.message.text, "hello from test");
|
|
93
|
+
assert.equal(result.message.sender, "user");
|
|
94
|
+
assert.equal(result.message.attachments.length, 1);
|
|
95
|
+
assert.equal(result.message.attachments[0].name, "x.png");
|
|
96
|
+
await ac.close();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3) Stale history replay must NOT satisfy the ack — even when the
|
|
100
|
+
// history contains a message that is IDENTICAL (sender, text,
|
|
101
|
+
// channel, reply_to) and has a recent timestamp (e.g. a retry of
|
|
102
|
+
// the same message sent <1s ago). Prior heuristic matcher was
|
|
103
|
+
// vulnerable to this; the fix uses the (1) status-frame history
|
|
104
|
+
// boundary and (2) strictly-greater-id correlation baseline so
|
|
105
|
+
// the historical echo is definitionally rejected. Reviewer1
|
|
106
|
+
// flagged this race on PR #382 round 1.
|
|
107
|
+
{
|
|
108
|
+
const stale = {
|
|
109
|
+
id: 42, sender: "user", text: "same words",
|
|
110
|
+
channel: "general", attachments: [], reply_to: null,
|
|
111
|
+
timestamp: Date.now() / 1000, // RIGHT NOW — old heuristic would accept this
|
|
112
|
+
};
|
|
113
|
+
const ac = await startFakeAc({ historyBeforeAck: [stale] });
|
|
114
|
+
const result = await sendViaWebSocket(ac.url, "fake-token", {
|
|
115
|
+
text: "same words",
|
|
116
|
+
sender: "user",
|
|
117
|
+
channel: "general",
|
|
118
|
+
attachments: [],
|
|
119
|
+
});
|
|
120
|
+
assert.ok(result.message.id > 42, `expected live echo id > 42, got ${result.message.id}`);
|
|
121
|
+
await ac.close();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 4) Premature close without ack rejects.
|
|
125
|
+
{
|
|
126
|
+
const ac = await startFakeAc({ dropBeforeAck: true });
|
|
127
|
+
let threw = false;
|
|
128
|
+
try {
|
|
129
|
+
await sendViaWebSocket(ac.url, "fake-token", {
|
|
130
|
+
text: "will drop", sender: "user", channel: "general", attachments: [],
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
threw = true;
|
|
134
|
+
assert.match(err.message, /closed before ack|websocket/);
|
|
135
|
+
assert.notEqual(err.code, "EAGENTCHATTR_401");
|
|
136
|
+
}
|
|
137
|
+
assert.equal(threw, true, "expected sendViaWebSocket to reject on premature close");
|
|
138
|
+
await ac.close();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 5) 4003 (bad token) rejects with EAGENTCHATTR_401.
|
|
142
|
+
{
|
|
143
|
+
const ac = await startFakeAc({ rejectWithCode: 4003 });
|
|
144
|
+
let threw = false;
|
|
145
|
+
try {
|
|
146
|
+
await sendViaWebSocket(ac.url, "bad-token", {
|
|
147
|
+
text: "denied", sender: "user", channel: "general", attachments: [],
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
threw = true;
|
|
151
|
+
assert.equal(err.code, "EAGENTCHATTR_401");
|
|
152
|
+
}
|
|
153
|
+
assert.equal(threw, true, "expected sendViaWebSocket to reject with EAGENTCHATTR_401");
|
|
154
|
+
await ac.close();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("routes.chatWsSend.test.js: all assertions passed (5 cases)");
|
|
158
|
+
})().catch((err) => {
|
|
159
|
+
console.error(err);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
package/server/routes.js
CHANGED
|
@@ -148,11 +148,41 @@ router.get("/api/chat", async (req, res) => {
|
|
|
148
148
|
const { WebSocket: NodeWebSocket } = require("ws");
|
|
149
149
|
const { syncChattrToken } = require("./config");
|
|
150
150
|
|
|
151
|
+
// #236: wait for AgentChattr to echo our message back over the same ws
|
|
152
|
+
// connection before resolving, instead of fire-and-forgetting. AC's
|
|
153
|
+
// /ws handler does this on every connect:
|
|
154
|
+
// 1. Replays history as N `{type:"message", data: msg}` frames.
|
|
155
|
+
// 2. Sends one `{type:"status", data: …}` frame (broadcast_status).
|
|
156
|
+
// 3. Enters the receive loop and accepts our outgoing frame.
|
|
157
|
+
// After our `type:"message"` is processed, AC calls `store.add()`
|
|
158
|
+
// which broadcasts the stored record back to all clients (including
|
|
159
|
+
// us) as another `{type:"message", data: msg}`.
|
|
160
|
+
//
|
|
161
|
+
// To get a race-free ack we therefore:
|
|
162
|
+
// A. Wait for the first `type:"status"` frame to confirm the
|
|
163
|
+
// history replay is done — any `type:"message"` frame seen
|
|
164
|
+
// BEFORE that is historical and must be ignored.
|
|
165
|
+
// B. Only then send our message and record the highest message
|
|
166
|
+
// id observed so far as a correlation baseline.
|
|
167
|
+
// C. Accept the first post-send `type:"message"` whose payload
|
|
168
|
+
// matches (sender, text, channel, reply_to) AND whose id is
|
|
169
|
+
// strictly greater than the baseline (AC ids are monotonically
|
|
170
|
+
// increasing from store.add). This eliminates the risk a
|
|
171
|
+
// reviewer flagged on #382 round 1: a historical identical
|
|
172
|
+
// message from <1.5s ago could have satisfied the old
|
|
173
|
+
// heuristic matcher.
|
|
174
|
+
// On timeout / early close / 4003, we surface a proper error so the
|
|
175
|
+
// /api/chat handler can return a 5xx (or 401) instead of a silent
|
|
176
|
+
// {ok:true}.
|
|
151
177
|
function sendViaWebSocket(baseUrl, sessionToken, message) {
|
|
152
178
|
return new Promise((resolve, reject) => {
|
|
153
179
|
const wsUrl = `${baseUrl.replace(/^http/, "ws")}/ws?token=${encodeURIComponent(sessionToken || "")}`;
|
|
154
180
|
const ws = new NodeWebSocket(wsUrl);
|
|
155
181
|
let settled = false;
|
|
182
|
+
let historyFlushed = false;
|
|
183
|
+
let sent = false;
|
|
184
|
+
let maxIdAtSend = -Infinity;
|
|
185
|
+
let maxHistoryId = -Infinity;
|
|
156
186
|
const finish = (err, value) => {
|
|
157
187
|
if (settled) return;
|
|
158
188
|
settled = true;
|
|
@@ -160,14 +190,56 @@ function sendViaWebSocket(baseUrl, sessionToken, message) {
|
|
|
160
190
|
if (err) reject(err); else resolve(value);
|
|
161
191
|
};
|
|
162
192
|
const giveUp = setTimeout(() => finish(new Error("websocket send timeout")), 4000);
|
|
163
|
-
|
|
193
|
+
const doSend = () => {
|
|
194
|
+
if (sent || settled) return;
|
|
164
195
|
try {
|
|
196
|
+
maxIdAtSend = maxHistoryId;
|
|
165
197
|
ws.send(JSON.stringify({ type: "message", ...message }));
|
|
166
|
-
|
|
167
|
-
// contract only needs to know the message was accepted. Wait
|
|
168
|
-
// ~250ms for the server to enqueue + close cleanly.
|
|
169
|
-
setTimeout(() => { clearTimeout(giveUp); finish(null, { ok: true }); }, 250);
|
|
198
|
+
sent = true;
|
|
170
199
|
} catch (err) { clearTimeout(giveUp); finish(err); }
|
|
200
|
+
};
|
|
201
|
+
ws.on("open", () => {
|
|
202
|
+
// Do NOT send yet. Wait for the status frame that marks the
|
|
203
|
+
// end of history replay so we have a clean correlation
|
|
204
|
+
// baseline. A safety timer covers the (unlikely) case of an
|
|
205
|
+
// AC build that doesn't emit status on connect — after 750ms
|
|
206
|
+
// we fall back to sending anyway, using whatever max id we
|
|
207
|
+
// collected from history so far as the baseline.
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
if (!historyFlushed) {
|
|
210
|
+
historyFlushed = true;
|
|
211
|
+
doSend();
|
|
212
|
+
}
|
|
213
|
+
}, 750);
|
|
214
|
+
});
|
|
215
|
+
ws.on("message", (raw) => {
|
|
216
|
+
if (settled) return;
|
|
217
|
+
let frame;
|
|
218
|
+
try { frame = JSON.parse(raw.toString()); } catch { return; }
|
|
219
|
+
if (!frame || !frame.type) return;
|
|
220
|
+
if (frame.type === "status" && !historyFlushed) {
|
|
221
|
+
historyFlushed = true;
|
|
222
|
+
doSend();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (frame.type !== "message" || !frame.data) return;
|
|
226
|
+
const d = frame.data;
|
|
227
|
+
// Track the highest message id we have observed, whether from
|
|
228
|
+
// history replay or from other live broadcasts. Used as the
|
|
229
|
+
// baseline for the post-send correlation check.
|
|
230
|
+
if (typeof d.id === "number" && d.id > maxHistoryId) {
|
|
231
|
+
maxHistoryId = d.id;
|
|
232
|
+
}
|
|
233
|
+
if (!sent) return; // anything before our send is history
|
|
234
|
+
if (typeof d.id !== "number" || d.id <= maxIdAtSend) return;
|
|
235
|
+
if (d.sender !== message.sender) return;
|
|
236
|
+
if (d.text !== message.text) return;
|
|
237
|
+
if ((d.channel || "general") !== (message.channel || "general")) return;
|
|
238
|
+
const wantReply = message.reply_to ?? null;
|
|
239
|
+
const gotReply = d.reply_to ?? null;
|
|
240
|
+
if (wantReply !== gotReply) return;
|
|
241
|
+
clearTimeout(giveUp);
|
|
242
|
+
finish(null, { ok: true, message: d });
|
|
171
243
|
});
|
|
172
244
|
ws.on("error", (err) => { clearTimeout(giveUp); finish(err); });
|
|
173
245
|
ws.on("close", (code, reason) => {
|
|
@@ -179,6 +251,15 @@ function sendViaWebSocket(baseUrl, sessionToken, message) {
|
|
|
179
251
|
const e = new Error(msg);
|
|
180
252
|
e.code = "EAGENTCHATTR_401";
|
|
181
253
|
finish(e);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Any other premature close after we sent but before we saw
|
|
257
|
+
// the echo is an error — the old code path would have claimed
|
|
258
|
+
// success, silently swallowing a server-side reject.
|
|
259
|
+
if (!settled) {
|
|
260
|
+
clearTimeout(giveUp);
|
|
261
|
+
const r = (reason && reason.toString()) || "";
|
|
262
|
+
finish(new Error(`websocket closed before ack (code=${code}${r ? ", reason=" + r : ""})`));
|
|
182
263
|
}
|
|
183
264
|
});
|
|
184
265
|
});
|
|
@@ -861,8 +942,12 @@ router.post("/api/chat", async (req, res) => {
|
|
|
861
942
|
const attemptSend = () => sendViaWebSocket(base, sessionToken, message);
|
|
862
943
|
|
|
863
944
|
try {
|
|
864
|
-
|
|
865
|
-
|
|
945
|
+
// #236: sendViaWebSocket now waits for AC's broadcast echo and
|
|
946
|
+
// returns `{ok, message}` where `message` is the stored record
|
|
947
|
+
// (with server-assigned id/timestamp). Pass it through so
|
|
948
|
+
// callers regain parity with the old /api/send response body.
|
|
949
|
+
const result = await attemptSend();
|
|
950
|
+
return res.json({ ok: true, message: result.message });
|
|
866
951
|
} catch (err) {
|
|
867
952
|
// If the cached session_token is stale (AgentChattr regenerates
|
|
868
953
|
// one on every restart) the ws closes with code 4003 — re-sync
|
|
@@ -876,8 +961,8 @@ router.post("/api/chat", async (req, res) => {
|
|
|
876
961
|
const { token: refreshed } = getChattrConfig(projectId);
|
|
877
962
|
if (refreshed && refreshed !== sessionToken) {
|
|
878
963
|
try {
|
|
879
|
-
await sendViaWebSocket(base, refreshed, message);
|
|
880
|
-
return res.json({ ok: true, resynced: true });
|
|
964
|
+
const retry = await sendViaWebSocket(base, refreshed, message);
|
|
965
|
+
return res.json({ ok: true, resynced: true, message: retry.message });
|
|
881
966
|
} catch (retryErr) {
|
|
882
967
|
console.warn(`[chat] retry after token resync failed: ${retryErr.message}`);
|
|
883
968
|
return res.status(401).json({ error: "AgentChattr auth failed (token resync did not help)", detail: retryErr.message });
|
|
@@ -2300,6 +2385,63 @@ function telegramConfigToml(projectId) {
|
|
|
2300
2385
|
return path.join(CONFIG_DIR, `telegram-${projectId}.toml`);
|
|
2301
2386
|
}
|
|
2302
2387
|
|
|
2388
|
+
// #383: path to a project's AgentChattr config.toml. The install
|
|
2389
|
+
// handler patches this file to declare the `telegram-bridge` agent
|
|
2390
|
+
// so AC's registry accepts the bridge's register call.
|
|
2391
|
+
function projectAgentchattrConfigPath(projectId) {
|
|
2392
|
+
return path.join(CONFIG_DIR, projectId, "agentchattr", "config.toml");
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// #383 Bug 1: prefer the per-project agentchattr_url. Every project
|
|
2396
|
+
// after the first uses a distinct port (8301, 8302, ...), so reading
|
|
2397
|
+
// the global default silently routed bridge traffic to the wrong AC
|
|
2398
|
+
// instance.
|
|
2399
|
+
function resolveProjectAgentchattrUrl(cfg, project) {
|
|
2400
|
+
return (
|
|
2401
|
+
(project && project.agentchattr_url) ||
|
|
2402
|
+
(cfg && cfg.agentchattr_url) ||
|
|
2403
|
+
"http://127.0.0.1:8300"
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// #383 Bug 2: the upstream bridge only reads `agentchattr_url` from
|
|
2408
|
+
// inside `[telegram]`. A separate `[agentchattr]` section is silently
|
|
2409
|
+
// ignored and the bridge falls back to its hardcoded :8300 default.
|
|
2410
|
+
function buildTelegramBridgeToml(tg) {
|
|
2411
|
+
return (
|
|
2412
|
+
`[telegram]\n` +
|
|
2413
|
+
`bot_token = "${tg.bot_token}"\n` +
|
|
2414
|
+
`chat_id = "${tg.chat_id}"\n` +
|
|
2415
|
+
`agentchattr_url = "${tg.agentchattr_url}"\n`
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// #383 Bug 3: AC's registry rejects any base name not pre-declared
|
|
2420
|
+
// in config.toml with `400 unknown base`. The bridge registers as
|
|
2421
|
+
// `telegram-bridge`, so every per-project AC config must declare it.
|
|
2422
|
+
// Idempotent: only appends if the section is not already present.
|
|
2423
|
+
function patchAgentchattrConfigForTelegramBridge(tomlText) {
|
|
2424
|
+
if (/^\[agents\.telegram-bridge\]\s*$/m.test(tomlText)) {
|
|
2425
|
+
return { text: tomlText, changed: false };
|
|
2426
|
+
}
|
|
2427
|
+
const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
|
|
2428
|
+
const block = `\n[agents.telegram-bridge]\nlabel = "Telegram Bridge"\n`;
|
|
2429
|
+
return { text: tomlText + sep + block, changed: true };
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// #383 Bug 4: the upstream bridge treats env vars as higher
|
|
2433
|
+
// precedence than TOML values. If the parent shell exported
|
|
2434
|
+
// TELEGRAM_BOT_TOKEN for a different bot, the bridge silently ran
|
|
2435
|
+
// as the wrong identity. Scrub those keys from the child's env so
|
|
2436
|
+
// the TOML is the single source of truth.
|
|
2437
|
+
function buildTelegramBridgeSpawnEnv(parentEnv) {
|
|
2438
|
+
const env = { ...parentEnv };
|
|
2439
|
+
delete env.TELEGRAM_BOT_TOKEN;
|
|
2440
|
+
delete env.TELEGRAM_CHAT_ID;
|
|
2441
|
+
delete env.AGENTCHATTR_URL;
|
|
2442
|
+
return env;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2303
2445
|
// #353: per-project log file for the bridge subprocess. The start
|
|
2304
2446
|
// handler redirects stdout + stderr here so crashes (ImportError,
|
|
2305
2447
|
// config parse, auth failure) are recoverable instead of
|
|
@@ -2339,7 +2481,12 @@ function readLastLines(filePath, n) {
|
|
|
2339
2481
|
// otherwise. Keep the import list small and close to what the
|
|
2340
2482
|
// bridge actually needs; add modules here if the bridge gains new
|
|
2341
2483
|
// hard deps.
|
|
2342
|
-
|
|
2484
|
+
// #380: `pythonPath` defaults to bare `python3` for backward-compat,
|
|
2485
|
+
// but the production call sites (install, start) MUST pass the
|
|
2486
|
+
// dedicated bridge venv's interpreter (`<BRIDGE_DIR>/.venv/bin/python3`)
|
|
2487
|
+
// so the import check runs against the same interpreter the spawn will
|
|
2488
|
+
// use. See #379 research ticket for root cause.
|
|
2489
|
+
function checkTelegramBridgePythonDeps(pythonPath = "python3") {
|
|
2343
2490
|
try {
|
|
2344
2491
|
// Only check the third-party module the bridge actually needs
|
|
2345
2492
|
// at import time — `requests`. Toml parsing differs between
|
|
@@ -2347,7 +2494,7 @@ function checkTelegramBridgePythonDeps() {
|
|
|
2347
2494
|
// genuine toml import failure will now be captured in the
|
|
2348
2495
|
// bridge log file on spawn, so this pre-flight stays narrow
|
|
2349
2496
|
// and avoids false negatives on older Python installs.
|
|
2350
|
-
execFileSync(
|
|
2497
|
+
execFileSync(pythonPath, ["-c", "import requests"], {
|
|
2351
2498
|
encoding: "utf-8",
|
|
2352
2499
|
timeout: 10000,
|
|
2353
2500
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -2412,7 +2559,8 @@ function getProjectTelegram(projectId) {
|
|
|
2412
2559
|
return {
|
|
2413
2560
|
bot_token: resolveToken(project.telegram.bot_token || ""),
|
|
2414
2561
|
chat_id: project.telegram.chat_id || "",
|
|
2415
|
-
|
|
2562
|
+
// #383 Bug 1: prefer per-project URL over the global default.
|
|
2563
|
+
agentchattr_url: resolveProjectAgentchattrUrl(cfg, project),
|
|
2416
2564
|
};
|
|
2417
2565
|
} catch {
|
|
2418
2566
|
return null;
|
|
@@ -2503,39 +2651,76 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2503
2651
|
}
|
|
2504
2652
|
}
|
|
2505
2653
|
case "install": {
|
|
2506
|
-
// #
|
|
2507
|
-
//
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
2654
|
+
// #380: create a dedicated bridge venv at
|
|
2655
|
+
// `<BRIDGE_DIR>/.venv` and install requirements into it using
|
|
2656
|
+
// that venv's pip. All bridge subprocesses then spawn with
|
|
2657
|
+
// `<BRIDGE_DIR>/.venv/bin/python3` by absolute path. See #379
|
|
2658
|
+
// research ticket for the root cause — bare `python3` / `pip3`
|
|
2659
|
+
// resolve to Homebrew Python on modern macOS where `requests`
|
|
2660
|
+
// is not available, producing a ModuleNotFoundError on Start.
|
|
2661
|
+
// Idempotent: existing installs missing a `.venv` get the venv
|
|
2662
|
+
// created on top of the existing clone without re-cloning.
|
|
2663
|
+
const venvDir = path.join(BRIDGE_DIR, ".venv");
|
|
2664
|
+
const venvPython = path.join(venvDir, "bin", "python3");
|
|
2665
|
+
const venvPip = path.join(venvDir, "bin", "pip");
|
|
2513
2666
|
let pipOutput = "";
|
|
2514
2667
|
try {
|
|
2515
2668
|
if (!fs.existsSync(BRIDGE_DIR)) {
|
|
2516
2669
|
execFileSync("gh", ["repo", "clone", "realproject7/agentchattr-telegram", BRIDGE_DIR], { encoding: "utf-8", timeout: 30000 });
|
|
2517
2670
|
}
|
|
2671
|
+
// #380: create the dedicated venv if missing. `python3 -m venv`
|
|
2672
|
+
// builds a fresh isolated environment that bypasses PEP 668
|
|
2673
|
+
// externally-managed markers, so this works even on Homebrew
|
|
2674
|
+
// Python where bare `pip3 install` would be blocked.
|
|
2675
|
+
if (!fs.existsSync(venvPython)) {
|
|
2676
|
+
execFileSync("python3", ["-m", "venv", venvDir], { encoding: "utf-8", timeout: 60000 });
|
|
2677
|
+
}
|
|
2518
2678
|
pipOutput = execFileSync(
|
|
2519
|
-
|
|
2679
|
+
venvPip,
|
|
2520
2680
|
["install", "-r", path.join(BRIDGE_DIR, "requirements.txt")],
|
|
2521
|
-
{ encoding: "utf-8", timeout:
|
|
2681
|
+
{ encoding: "utf-8", timeout: 120000 },
|
|
2522
2682
|
);
|
|
2523
2683
|
} catch (err) {
|
|
2524
|
-
|
|
2684
|
+
const stderr = (err && err.stderr && err.stderr.toString && err.stderr.toString()) || "";
|
|
2685
|
+
return res.json({ ok: false, error: (stderr.trim() || err.message || "Install failed") });
|
|
2525
2686
|
}
|
|
2526
|
-
const depCheck = checkTelegramBridgePythonDeps();
|
|
2687
|
+
const depCheck = checkTelegramBridgePythonDeps(venvPython);
|
|
2527
2688
|
if (!depCheck.ok) {
|
|
2528
2689
|
return res.json({
|
|
2529
2690
|
ok: false,
|
|
2530
2691
|
error:
|
|
2531
|
-
"
|
|
2532
|
-
"This
|
|
2533
|
-
|
|
2692
|
+
"pip reported success but the bridge venv's Python deps still fail to import. " +
|
|
2693
|
+
"This is unexpected for a freshly-created venv — check disk space and permissions " +
|
|
2694
|
+
`on ${venvDir}.\n\n` +
|
|
2534
2695
|
`Import error: ${depCheck.error}\n\n` +
|
|
2535
2696
|
`pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
|
|
2536
2697
|
});
|
|
2537
2698
|
}
|
|
2538
|
-
|
|
2699
|
+
// #383 Bug 3: ensure every known project's AC config declares
|
|
2700
|
+
// the `telegram-bridge` agent. Without this, AC's registry
|
|
2701
|
+
// rejects the bridge's register call with `400 unknown base`
|
|
2702
|
+
// and the bridge enters an infinite re-register loop.
|
|
2703
|
+
// Idempotent — append-only, skips configs that already have
|
|
2704
|
+
// the section. Does NOT restart AC servers; the operator
|
|
2705
|
+
// must click SERVER → Restart to load the new agent slug.
|
|
2706
|
+
const patched = [];
|
|
2707
|
+
try {
|
|
2708
|
+
const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
2709
|
+
for (const proj of cfgAll.projects || []) {
|
|
2710
|
+
if (!proj || !proj.id) continue;
|
|
2711
|
+
const acPath = projectAgentchattrConfigPath(proj.id);
|
|
2712
|
+
if (!fs.existsSync(acPath)) continue;
|
|
2713
|
+
try {
|
|
2714
|
+
const before = fs.readFileSync(acPath, "utf-8");
|
|
2715
|
+
const { text, changed } = patchAgentchattrConfigForTelegramBridge(before);
|
|
2716
|
+
if (changed) {
|
|
2717
|
+
fs.writeFileSync(acPath, text);
|
|
2718
|
+
patched.push(proj.id);
|
|
2719
|
+
}
|
|
2720
|
+
} catch {}
|
|
2721
|
+
}
|
|
2722
|
+
} catch {}
|
|
2723
|
+
return res.json({ ok: true, patched_projects: patched });
|
|
2539
2724
|
}
|
|
2540
2725
|
case "start": {
|
|
2541
2726
|
const projectId = body.project_id;
|
|
@@ -2543,17 +2728,31 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2543
2728
|
if (isTelegramRunning(projectId)) return res.json({ ok: true, running: true, message: "Already running" });
|
|
2544
2729
|
const bridgeScript = path.join(BRIDGE_DIR, "telegram_bridge.py");
|
|
2545
2730
|
if (!fs.existsSync(bridgeScript)) return res.json({ ok: false, error: "Bridge not installed. Click Install Bridge first." });
|
|
2731
|
+
// #380: resolve the dedicated venv's python3 by absolute path.
|
|
2732
|
+
// Do NOT activate the venv or set VIRTUAL_ENV in the parent —
|
|
2733
|
+
// calling the venv's python3 directly is sufficient because
|
|
2734
|
+
// Python's sys.executable bootstrap resolves the venv
|
|
2735
|
+
// automatically. See #379 research ticket.
|
|
2736
|
+
const venvPython = path.join(BRIDGE_DIR, ".venv", "bin", "python3");
|
|
2737
|
+
if (!fs.existsSync(venvPython)) {
|
|
2738
|
+
return res.json({
|
|
2739
|
+
ok: false,
|
|
2740
|
+
error: "Bridge venv missing. Click \"Install Bridge\" to create it.",
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2546
2743
|
const tg = getProjectTelegram(projectId);
|
|
2547
2744
|
if (!tg || !tg.bot_token || !tg.chat_id) return res.json({ ok: false, error: "Save bot_token and chat_id in project settings first." });
|
|
2548
2745
|
const tomlPath = telegramConfigToml(projectId);
|
|
2549
|
-
|
|
2746
|
+
// #383 Bug 2: write agentchattr_url inside [telegram]; the
|
|
2747
|
+
// bridge's load_config only reads from that section.
|
|
2748
|
+
const tomlContent = buildTelegramBridgeToml(tg);
|
|
2550
2749
|
fs.writeFileSync(tomlPath, tomlContent, { mode: 0o600 });
|
|
2551
2750
|
fs.chmodSync(tomlPath, 0o600);
|
|
2552
2751
|
// #353: pre-flight import check so a fresh install with no
|
|
2553
2752
|
// `requests` module produces a readable error instead of the
|
|
2554
2753
|
// Start → Running → Stopped flicker that the v1 code path
|
|
2555
2754
|
// produced with `stdio: "ignore"`.
|
|
2556
|
-
const depCheck = checkTelegramBridgePythonDeps();
|
|
2755
|
+
const depCheck = checkTelegramBridgePythonDeps(venvPython);
|
|
2557
2756
|
if (!depCheck.ok) {
|
|
2558
2757
|
// #372: persist the pre-flight failure to the bridge log
|
|
2559
2758
|
// file so the GET /api/telegram `last_error` tail picks it
|
|
@@ -2562,8 +2761,8 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2562
2761
|
// local error state, producing the "silent fail" symptom
|
|
2563
2762
|
// (pill flips back to Stopped with no trace of why).
|
|
2564
2763
|
const msg =
|
|
2565
|
-
"Bridge Python dependencies not installed
|
|
2566
|
-
"
|
|
2764
|
+
"Bridge Python dependencies not installed in the dedicated venv. " +
|
|
2765
|
+
"Click \"Install Bridge\" to (re)create the venv and install them.\n\n" +
|
|
2567
2766
|
`Import error: ${depCheck.error}`;
|
|
2568
2767
|
try {
|
|
2569
2768
|
fs.writeFileSync(
|
|
@@ -2595,9 +2794,15 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2595
2794
|
}
|
|
2596
2795
|
let child;
|
|
2597
2796
|
try {
|
|
2598
|
-
|
|
2797
|
+
// #383 Bug 4: scrub TELEGRAM_*/AGENTCHATTR_URL from the child
|
|
2798
|
+
// env so an operator shell that exports a different bot's
|
|
2799
|
+
// token (common on machines running AC2) can't silently
|
|
2800
|
+
// override the TOML. Makes the TOML the single source of
|
|
2801
|
+
// truth for the bridge's identity.
|
|
2802
|
+
child = spawn(venvPython, [bridgeScript, "--config", tomlPath], {
|
|
2599
2803
|
detached: true,
|
|
2600
2804
|
stdio: ["ignore", outFd, errFd],
|
|
2805
|
+
env: buildTelegramBridgeSpawnEnv(process.env),
|
|
2601
2806
|
});
|
|
2602
2807
|
child.unref();
|
|
2603
2808
|
if (child.pid) fs.writeFileSync(telegramPidFile(projectId), String(child.pid));
|
|
@@ -2780,3 +2985,16 @@ module.exports.buildNoPrRow = buildNoPrRow;
|
|
|
2780
2985
|
module.exports.summarizeItems = summarizeItems;
|
|
2781
2986
|
// #353: expose readLastLines for the telegram-bridge test.
|
|
2782
2987
|
module.exports.readLastLines = readLastLines;
|
|
2988
|
+
// #380: expose checkTelegramBridgePythonDeps so the bridge test can
|
|
2989
|
+
// exercise the venv-path interpreter argument round trip.
|
|
2990
|
+
module.exports.checkTelegramBridgePythonDeps = checkTelegramBridgePythonDeps;
|
|
2991
|
+
// #383: pure helpers exposed for unit tests in
|
|
2992
|
+
// routes.telegramBridge.test.js. No production callers outside
|
|
2993
|
+
// this file.
|
|
2994
|
+
module.exports.resolveProjectAgentchattrUrl = resolveProjectAgentchattrUrl;
|
|
2995
|
+
module.exports.buildTelegramBridgeToml = buildTelegramBridgeToml;
|
|
2996
|
+
module.exports.patchAgentchattrConfigForTelegramBridge = patchAgentchattrConfigForTelegramBridge;
|
|
2997
|
+
module.exports.buildTelegramBridgeSpawnEnv = buildTelegramBridgeSpawnEnv;
|
|
2998
|
+
// #236: expose sendViaWebSocket so the chat-ws-send regression test
|
|
2999
|
+
// can verify the ack/body/error paths against a fake AC ws server.
|
|
3000
|
+
module.exports.sendViaWebSocket = sendViaWebSocket;
|