pi-harness-runtime 1.1.390 → 1.1.392
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/AGENTS.md +38 -11
- package/harness/startup-report.ts +36 -6
- package/harness/telegram-auth.ts +32 -0
- package/harness/telegram-update.ts +125 -0
- package/index.ts +122 -29
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/workspace-dotenv.d.ts +44 -0
- package/packages/contracts/dist/workspace-dotenv.js +111 -0
- package/packages/contracts/dist/workspace-env-format.d.ts +185 -9
- package/packages/contracts/dist/workspace-env-format.js +402 -33
- package/packages/contracts/dist/workspace-env.d.ts +10 -6
- package/packages/contracts/dist/workspace-env.js +459 -112
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/workspace-dotenv.ts +114 -0
- package/packages/contracts/src/workspace-env-format.ts +464 -29
- package/packages/contracts/src/workspace-env.ts +503 -114
- package/packages/contracts/test/workspace-env.test.ts +492 -11
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/line-adapter.d.ts +39 -0
- package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.js +119 -8
- package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.d.ts +95 -0
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +284 -18
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/base-adapter.d.ts +20 -0
- package/packages/notification/dist/base-adapter.d.ts.map +1 -1
- package/packages/notification/dist/base-adapter.js +17 -0
- package/packages/notification/dist/base-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts +8 -0
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +11 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/telegram-auth.d.ts +23 -0
- package/packages/notification/dist/telegram-auth.d.ts.map +1 -0
- package/packages/notification/dist/telegram-auth.js +32 -0
- package/packages/notification/dist/telegram-auth.js.map +1 -0
- package/packages/notification/dist/types.d.ts +5 -0
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/line-adapter.ts +145 -13
- package/packages/notification/src/adapters/telegram-adapter.ts +283 -18
- package/packages/notification/src/base-adapter.ts +28 -0
- package/packages/notification/src/notification-center.ts +12 -0
- package/packages/notification/src/telegram-auth.ts +34 -0
- package/packages/notification/src/types.ts +5 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/server.js +7 -2
- package/packages/relay/dist/types.d.ts +3 -1
- package/packages/relay/dist/ws-server.d.ts +2 -9
- package/packages/relay/dist/ws-server.js +5 -3
- package/packages/relay/package.json +1 -1
- package/packages/relay/src/server.ts +7 -2
- package/packages/relay/src/types.ts +6 -1
- package/packages/relay/src/ws-server.ts +9 -13
- package/packages/relay/test/relay-e2e.test.ts +36 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/dist/harness-footer.d.ts +23 -8
- package/packages/tui/dist/harness-footer.d.ts.map +1 -1
- package/packages/tui/dist/harness-footer.js +144 -2
- package/packages/tui/dist/harness-footer.js.map +1 -1
- package/packages/tui/dist/index.d.ts +1 -1
- package/packages/tui/dist/index.d.ts.map +1 -1
- package/packages/tui/dist/index.js +1 -1
- package/packages/tui/dist/index.js.map +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/tui/src/harness-footer.ts +149 -2
- package/packages/tui/src/index.ts +1 -0
- package/packages/tui/test/harness-footer.test.ts +134 -15
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/unified-webhook-server.ts +141 -14
- package/scripts/ws-server.ts +7 -1
package/AGENTS.md
CHANGED
|
@@ -494,36 +494,63 @@ bun run build && bun run start # Server deployment
|
|
|
494
494
|
### 14. Workspace Environment Auto-Detection
|
|
495
495
|
|
|
496
496
|
**At session start, the harness auto-detects:**
|
|
497
|
-
- `.env` file
|
|
497
|
+
- `.env` / `.env.*` file variables → Dev environment evidence
|
|
498
498
|
- `package.json` `dev` script → Hot reload capability
|
|
499
|
-
- `NODE_ENV`
|
|
499
|
+
- The environment variable, in this order: the `NODE_ENV` of the process env
|
|
500
|
+
(it decides, also when it names no known mode), then the dotenv files of the
|
|
501
|
+
workspace: `NODE_ENV`, an environment shaped key whose value names a known
|
|
502
|
+
mode (`*_ENV`, `*_ENVIRONMENT`, `*_MODE`, `*_STAGE`, e.g. `AMOS_ENV`), then
|
|
503
|
+
any other environment shaped key - and only then an environment shaped key
|
|
504
|
+
of the process env → development/production/test
|
|
500
505
|
- System RAM → Low RAM warning (<16GB total or <4GB available)
|
|
501
506
|
- Running processes → dev/build server status
|
|
502
507
|
|
|
508
|
+
The dotenv files are read from the WORKSPACE (not from the current working
|
|
509
|
+
directory), the template files (`.env.example`, `.env.sample`,
|
|
510
|
+
`.env.template`, ...) are ignored and a file bigger than 256 kB is not read.
|
|
511
|
+
A value that is not provably harmless is masked (`<set>`): only the
|
|
512
|
+
`NODE_ENV` of the process env and a value that IS the name of the mode are
|
|
513
|
+
shown. A key that only looks like a branch (`BRANCH=production`) never
|
|
514
|
+
decides the mode.
|
|
515
|
+
|
|
503
516
|
**This info is shown in UI and delivered to agent context:**
|
|
504
517
|
```
|
|
505
|
-
Workspace: DEVELOPMENT | dev server: running |
|
|
518
|
+
Workspace: ~/pi-harness-runtime | OS: Linux | mode: DEVELOPMENT (from AMOS_ENV in .env) | dev server: running | dev script: available | build script: available | RAM: 10/16
|
|
506
519
|
```
|
|
507
520
|
|
|
508
521
|
The mode words are exactly what the detector emits: `DEVELOPMENT`, `PRODUCTION`,
|
|
509
|
-
`TEST` or `UNKNOWN` (with its reason, e.g.
|
|
510
|
-
|
|
511
|
-
|
|
522
|
+
`TEST` or `UNKNOWN` (with its reason, e.g.
|
|
523
|
+
`UNKNOWN (no NODE_ENV or environment variable found)` or
|
|
524
|
+
`UNKNOWN (AMOS_ENV in .env names no known mode)`). A known mode also
|
|
525
|
+
names its source: `DEVELOPMENT (from AMOS_ENV in .env)`.
|
|
526
|
+
A dev/build script is a CAPABILITY, not a state: `dev script: available` means
|
|
527
|
+
the script exists, not that a server runs. The server state is separate and
|
|
528
|
+
never claims more than it knows: `running`, `not detected` (a check ran and
|
|
529
|
+
found nothing) or `not checked` (no check was possible, e.g. no `pgrep`).
|
|
530
|
+
|
|
531
|
+
The OS is part of the line because it decides which command prints the
|
|
532
|
+
working directory: `pwd` on Linux/macOS, `(pwd).Path` or `$pwd.Path` in
|
|
533
|
+
PowerShell (the plain `pwd` there prints a "Path: " header), `cd` or
|
|
534
|
+
`echo %cd%` in the Windows command prompt. The agent context names the
|
|
535
|
+
command to use.
|
|
512
536
|
|
|
513
537
|
**IMPORTANT Rules:**
|
|
514
538
|
|
|
515
539
|
| State | Action |
|
|
516
540
|
|-------|--------|
|
|
517
|
-
| `
|
|
518
|
-
| `
|
|
519
|
-
| `
|
|
541
|
+
| `mode: UNKNOWN (…)` | Explicitly ask user before build |
|
|
542
|
+
| `mode: DEVELOPMENT` | Use `dev` mode, don't auto-build |
|
|
543
|
+
| `mode: PRODUCTION` | Build allowed, but confirm with user |
|
|
520
544
|
| `Low RAM` | Never run parallel build + dev |
|
|
521
545
|
| `Dev server running` | Reuse it, don't restart |
|
|
522
546
|
|
|
523
547
|
**Decision flow:**
|
|
524
548
|
```
|
|
525
|
-
Check process.env.NODE_ENV first
|
|
526
|
-
Check .env
|
|
549
|
+
Check process.env.NODE_ENV first (it decides, also when it names no known mode)
|
|
550
|
+
Check the .env and .env.* files of the workspace second
|
|
551
|
+
(NODE_ENV, then an environment shaped key whose value names a mode,
|
|
552
|
+
then any other environment shaped key)
|
|
553
|
+
Only then an environment shaped key of the process env (e.g. AMOS_ENV)
|
|
527
554
|
Presence of dev script = capability, not proof of mode
|
|
528
555
|
UNKNOWN requires investigation, not auto-build
|
|
529
556
|
Production detection ≠ deployment authorization
|
|
@@ -59,7 +59,10 @@ export function oneLineError(error: unknown, maxLength = 160): string {
|
|
|
59
59
|
"$1<redacted>",
|
|
60
60
|
)
|
|
61
61
|
// A url path: "/bot123456:ABC-DEF/", "/token/abc12345".
|
|
62
|
-
|
|
62
|
+
// The boundary is "not a digit" and not a word boundary, so a
|
|
63
|
+
// token that is glued to a word ("xbot123:ABC-DEF") is redacted
|
|
64
|
+
// as well.
|
|
65
|
+
.replace(/(^|[^0-9])\d{6,}:[A-Za-z0-9_\-]{4,}/g, "$1<redacted>")
|
|
63
66
|
.replace(
|
|
64
67
|
/(\/((?:access_token|api[-_]?key|token|secret)[=/]))[A-Za-z0-9_\-:]{4,}/gi,
|
|
65
68
|
"$1<redacted>",
|
|
@@ -177,6 +180,21 @@ export function lineConfigurationReport(options: {
|
|
|
177
180
|
telegramConfigured?: boolean;
|
|
178
181
|
/** The fix commands for the missing LINE credentials. */
|
|
179
182
|
fixes: string[];
|
|
183
|
+
/**
|
|
184
|
+
* The fix commands for the channel access token only.
|
|
185
|
+
*
|
|
186
|
+
* The startup check validates the ACCESS TOKEN. The user id is only the
|
|
187
|
+
* recipient of the messages, so a failed check must not point at
|
|
188
|
+
* line-user-id.txt.
|
|
189
|
+
*/
|
|
190
|
+
accessTokenFixes?: string[];
|
|
191
|
+
/**
|
|
192
|
+
* Why the channel did not start (from the adapter).
|
|
193
|
+
*
|
|
194
|
+
* It says WHICH problem it is - an invalid or revoked token, a LINE API
|
|
195
|
+
* failure or a network failure - and it never contains a credential.
|
|
196
|
+
*/
|
|
197
|
+
channelDiagnostic?: string | null;
|
|
180
198
|
}): StartupReport | null {
|
|
181
199
|
if (!options.clientKeyFilePresent) return null;
|
|
182
200
|
const channelState = options.channelState ?? "not-started";
|
|
@@ -204,15 +222,27 @@ export function lineConfigurationReport(options: {
|
|
|
204
222
|
}
|
|
205
223
|
if (channelState === "failed") {
|
|
206
224
|
// The credentials are there but the channel did not start - the
|
|
207
|
-
//
|
|
225
|
+
// channel access token is what the startup check validates, so
|
|
226
|
+
// the user id must not be named as the cause.
|
|
227
|
+
const details = [
|
|
228
|
+
...(options.accessTokenFixes && options.accessTokenFixes.length > 0
|
|
229
|
+
? options.accessTokenFixes
|
|
230
|
+
: options.fixes),
|
|
231
|
+
];
|
|
232
|
+
if (options.channelDiagnostic) {
|
|
233
|
+
details.push(options.channelDiagnostic);
|
|
234
|
+
}
|
|
235
|
+
details.push(
|
|
236
|
+
"The startup check validates the channel access token - the user id is only the recipient of the messages.",
|
|
237
|
+
);
|
|
238
|
+
details.push(
|
|
239
|
+
"Fix the file, then reload Pi so the channel is checked again.",
|
|
240
|
+
);
|
|
208
241
|
return {
|
|
209
242
|
label: "LINE",
|
|
210
243
|
text: "configured but the LINE channel did not start",
|
|
211
244
|
status: "warn",
|
|
212
|
-
details
|
|
213
|
-
...options.fixes,
|
|
214
|
-
"Check the LINE credentials in ~/.pi-harness-runtime/keys/, then reload Pi.",
|
|
215
|
-
],
|
|
245
|
+
details,
|
|
216
246
|
};
|
|
217
247
|
}
|
|
218
248
|
// The credentials are there but the channel was never started in this
|
package/harness/telegram-auth.ts
CHANGED
|
@@ -90,6 +90,38 @@ export function getAllowedUserIds(): Set<number> {
|
|
|
90
90
|
return new Set();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Describe who may write into Pi through Telegram.
|
|
95
|
+
*
|
|
96
|
+
* The allow-list falls back to the chat id file, and a GROUP chat id is
|
|
97
|
+
* negative - it identifies a chat, not a person. A sender rule that only
|
|
98
|
+
* knows a group id lets nobody in, silently. The description makes that
|
|
99
|
+
* visible.
|
|
100
|
+
*/
|
|
101
|
+
export function describeInboundAccess(
|
|
102
|
+
allowedUserIds: Iterable<number>,
|
|
103
|
+
chatId?: string,
|
|
104
|
+
): string {
|
|
105
|
+
const ids = [...allowedUserIds].filter((n) => Number.isFinite(n));
|
|
106
|
+
const users = ids.filter((n) => n > 0);
|
|
107
|
+
const groups = ids.filter((n) => n < 0);
|
|
108
|
+
if (users.length === 0) {
|
|
109
|
+
const known = groups.length > 0 ? ` (the allow-list holds the group chat id ${groups.join(", ")})` : "";
|
|
110
|
+
return `nobody may write in${known}`;
|
|
111
|
+
}
|
|
112
|
+
const chat = chatId ? ` in chat ${chatId}` : "";
|
|
113
|
+
return `user id(s) ${users.join(", ")} may write in${chat}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decide whether a Telegram sender may write into Pi.
|
|
118
|
+
*
|
|
119
|
+
* The rule itself lives in the notification package
|
|
120
|
+
* (packages/notification/src/telegram-auth.ts) so that the adapter, this
|
|
121
|
+
* module and the remote webhook server all use ONE implementation.
|
|
122
|
+
*/
|
|
123
|
+
export { isAuthorizedTelegramSender } from "../packages/notification/dist/telegram-auth.js";
|
|
124
|
+
|
|
93
125
|
/**
|
|
94
126
|
* Check if a Telegram user ID is authorized
|
|
95
127
|
*/
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a relay "telegram_message" payload onto a Telegram update.
|
|
3
|
+
*
|
|
4
|
+
* The mapping lives in a pure module so that it can be tested without the
|
|
5
|
+
* harness: the relay carries the text, the adapter decides whether the
|
|
6
|
+
* sender is allowed to write in.
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* - the text is never put into an id field (ids are logged by the client)
|
|
10
|
+
* - the real bot flag has to survive the transport, otherwise the adapter
|
|
11
|
+
* could not refuse bot messages
|
|
12
|
+
* - a payload without text produces no update
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface TelegramMessageUpdate {
|
|
16
|
+
message: {
|
|
17
|
+
message_id: number;
|
|
18
|
+
from: {
|
|
19
|
+
id: number;
|
|
20
|
+
is_bot: boolean;
|
|
21
|
+
first_name: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
};
|
|
24
|
+
chat: { id: number };
|
|
25
|
+
text: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TelegramMessagePayload {
|
|
30
|
+
userId: number;
|
|
31
|
+
username?: string;
|
|
32
|
+
messageId?: number;
|
|
33
|
+
chatId?: number;
|
|
34
|
+
text?: string;
|
|
35
|
+
isBot?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TelegramCallbackUpdate {
|
|
39
|
+
callback_query: {
|
|
40
|
+
id: string;
|
|
41
|
+
from: {
|
|
42
|
+
id: number;
|
|
43
|
+
is_bot: boolean;
|
|
44
|
+
first_name: string;
|
|
45
|
+
username?: string;
|
|
46
|
+
};
|
|
47
|
+
data: string;
|
|
48
|
+
message: {
|
|
49
|
+
chat: { id: number };
|
|
50
|
+
message_id: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the Telegram update for a relayed button callback.
|
|
57
|
+
*
|
|
58
|
+
* The real bot flag has to survive the transport: the adapter refuses
|
|
59
|
+
* messages and callbacks of a bot.
|
|
60
|
+
*/
|
|
61
|
+
export function buildTelegramCallbackUpdate(
|
|
62
|
+
payload: TelegramMessagePayload & {
|
|
63
|
+
action?: string;
|
|
64
|
+
targetId?: string;
|
|
65
|
+
callbackQueryId?: string;
|
|
66
|
+
id?: string;
|
|
67
|
+
},
|
|
68
|
+
): TelegramCallbackUpdate | null {
|
|
69
|
+
const userId = Number(payload.userId);
|
|
70
|
+
if (!Number.isFinite(userId)) return null;
|
|
71
|
+
const chatId = Number(payload.chatId);
|
|
72
|
+
const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
|
|
73
|
+
return {
|
|
74
|
+
callback_query: {
|
|
75
|
+
id: payload.callbackQueryId || String(payload.id || ""),
|
|
76
|
+
from: {
|
|
77
|
+
id: userId,
|
|
78
|
+
is_bot: payload.isBot === true,
|
|
79
|
+
first_name: payload.username || "User",
|
|
80
|
+
username: payload.username,
|
|
81
|
+
},
|
|
82
|
+
data: `${payload.action || ""}:${payload.targetId || ""}`,
|
|
83
|
+
message: {
|
|
84
|
+
chat: { id: effectiveChatId },
|
|
85
|
+
message_id: Number(payload.messageId) || 0,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the Telegram update for a relayed text message.
|
|
93
|
+
*
|
|
94
|
+
* Returns null when the payload carries no text - there is nothing to
|
|
95
|
+
* forward and the adapter must not see an empty message.
|
|
96
|
+
*/
|
|
97
|
+
export function buildTelegramMessageUpdate(
|
|
98
|
+
payload: TelegramMessagePayload,
|
|
99
|
+
): TelegramMessageUpdate | null {
|
|
100
|
+
const text = payload.text ?? "";
|
|
101
|
+
if (!text) return null;
|
|
102
|
+
|
|
103
|
+
const userId = Number(payload.userId);
|
|
104
|
+
if (!Number.isFinite(userId)) return null;
|
|
105
|
+
|
|
106
|
+
// A direct message has the chat id of the user; the remote always sends
|
|
107
|
+
// the real chat id, so the fallback is only for a DM.
|
|
108
|
+
const chatId = Number(payload.chatId);
|
|
109
|
+
const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
message: {
|
|
113
|
+
message_id: Number(payload.messageId) || 0,
|
|
114
|
+
from: {
|
|
115
|
+
id: userId,
|
|
116
|
+
// The real flag: the adapter refuses bot messages.
|
|
117
|
+
is_bot: payload.isBot === true,
|
|
118
|
+
first_name: payload.username || "User",
|
|
119
|
+
username: payload.username,
|
|
120
|
+
},
|
|
121
|
+
chat: { id: effectiveChatId },
|
|
122
|
+
text,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
type KeyFileAuditResult,
|
|
29
29
|
} from "./harness/key-file-audit.ts";
|
|
30
30
|
import { renderDetailLines } from "./harness/startup-lines.ts";
|
|
31
|
+
import { describeInboundAccess, getAllowedUserIds } from "./harness/telegram-auth.ts";
|
|
32
|
+
import { buildTelegramCallbackUpdate, buildTelegramMessageUpdate } from "./harness/telegram-update.ts";
|
|
31
33
|
import {
|
|
32
34
|
jevMissingKeyReport,
|
|
33
35
|
lineConfigurationReport,
|
|
@@ -163,6 +165,15 @@ let keyFileAuditDone = false;
|
|
|
163
165
|
* startup line must not claim a green "configured".
|
|
164
166
|
*/
|
|
165
167
|
let lineChannelState: "started" | "failed" | "not-started" = "not-started";
|
|
168
|
+
/**
|
|
169
|
+
* Why the LINE channel did not start (from the adapter).
|
|
170
|
+
*
|
|
171
|
+
* The adapter knows the difference between an invalid or revoked token
|
|
172
|
+
* (401/403), a LINE API failure and a network failure - the startup
|
|
173
|
+
* summary shows it so the user knows what to fix. It never contains a
|
|
174
|
+
* credential.
|
|
175
|
+
*/
|
|
176
|
+
let lineChannelDiagnostic: string | null = null;
|
|
166
177
|
let telegramChannelState: "started" | "failed" | "not-started" = "not-started";
|
|
167
178
|
/** Did the relay client connect at least once in this extension load? */
|
|
168
179
|
let relayStreamConnected = false;
|
|
@@ -1355,6 +1366,7 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
|
|
|
1355
1366
|
return;
|
|
1356
1367
|
}
|
|
1357
1368
|
|
|
1369
|
+
const allowedUserIds = [...getAllowedUserIds()];
|
|
1358
1370
|
const channels: Array<{
|
|
1359
1371
|
id: string;
|
|
1360
1372
|
type: string;
|
|
@@ -1365,10 +1377,27 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
|
|
|
1365
1377
|
id: "telegram",
|
|
1366
1378
|
type: "telegram",
|
|
1367
1379
|
enabled: true,
|
|
1368
|
-
config: { botToken, chatId },
|
|
1380
|
+
config: { botToken, chatId, allowedUserIds },
|
|
1369
1381
|
},
|
|
1370
1382
|
];
|
|
1371
1383
|
|
|
1384
|
+
// Who may write into Pi? A group chat id is not a user id: when the
|
|
1385
|
+
// allow-list holds only a group id, the inbound path is closed - and
|
|
1386
|
+
// that must be visible, not silent.
|
|
1387
|
+
const inboundAccess = describeInboundAccess(allowedUserIds, chatId);
|
|
1388
|
+
logStartup(`[pi-harness] Telegram inbound: ${inboundAccess}`);
|
|
1389
|
+
if (inboundAccess.startsWith("nobody")) {
|
|
1390
|
+
addHarnessMessage(
|
|
1391
|
+
"Telegram",
|
|
1392
|
+
"nobody may write in: the allow-list holds no user id",
|
|
1393
|
+
"warn",
|
|
1394
|
+
[
|
|
1395
|
+
"Write your Telegram user id into ~/.pi-harness-runtime/keys/telegram-allowed-users.txt (a chat id of a group is not a user id).",
|
|
1396
|
+
"Then reload Pi.",
|
|
1397
|
+
],
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1372
1401
|
// Add LINE channel if credentials exist
|
|
1373
1402
|
const lineChannelConfigured = Boolean(hasLineCredentials && lineAccessToken && lineUserId);
|
|
1374
1403
|
if (lineChannelConfigured) {
|
|
@@ -1398,9 +1427,13 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
|
|
|
1398
1427
|
if (lineChannelConfigured) {
|
|
1399
1428
|
if (center.isChannelHealthy("line")) {
|
|
1400
1429
|
lineChannelState = "started";
|
|
1430
|
+
lineChannelDiagnostic = null;
|
|
1401
1431
|
addHarnessMessage("LINE", "configured and enabled (tap Toggle LINE to disable)", "ok");
|
|
1402
1432
|
} else {
|
|
1403
1433
|
lineChannelState = "failed";
|
|
1434
|
+
// Keep the reason: "the token is wrong" and "LINE is down"
|
|
1435
|
+
// need different actions.
|
|
1436
|
+
lineChannelDiagnostic = center.getChannelDiagnostic("line");
|
|
1404
1437
|
}
|
|
1405
1438
|
}
|
|
1406
1439
|
if (telegramChannelState !== "started") {
|
|
@@ -1574,7 +1607,9 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
|
|
|
1574
1607
|
return;
|
|
1575
1608
|
}
|
|
1576
1609
|
|
|
1577
|
-
|
|
1610
|
+
// The user's own words stay out of the log files: only the length
|
|
1611
|
+
// is recorded.
|
|
1612
|
+
logToFile("[TelegramInbound]", `Forwarding message to Pi (${text.length} chars)`);
|
|
1578
1613
|
// Forward the message to Pi as a user message
|
|
1579
1614
|
await pi.sendUserMessage(text, { deliverAs: "steer" });
|
|
1580
1615
|
});
|
|
@@ -1631,7 +1666,32 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
|
|
|
1631
1666
|
]);
|
|
1632
1667
|
return;
|
|
1633
1668
|
}
|
|
1634
|
-
|
|
1669
|
+
// Polling and the remote webhook are mutually exclusive. Telegram
|
|
1670
|
+
// rejects getUpdates while a webhook is active, and the rejected poll
|
|
1671
|
+
// also terminates the getUpdates stream of every other poller of this
|
|
1672
|
+
// bot, which delayed the inbound text messages by seconds.
|
|
1673
|
+
if (telegram) {
|
|
1674
|
+
// A 409 (a webhook or another poller owns this bot) stops the
|
|
1675
|
+
// inbound path. The user must see it - a silent 409 looks like a
|
|
1676
|
+
// working inbound path.
|
|
1677
|
+
telegram.onPollStopped = (reason) => {
|
|
1678
|
+
addHarnessMessage("Telegram", `inbound polling stopped: ${reason}`, "warn", [
|
|
1679
|
+
"Telegram rejects getUpdates (409) while a webhook or another poller owns this bot.",
|
|
1680
|
+
"Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
|
|
1681
|
+
]);
|
|
1682
|
+
};
|
|
1683
|
+
if (!telegram.isWebhookConfigured()) {
|
|
1684
|
+
telegram.startPolling();
|
|
1685
|
+
// The poll may already have been rejected (a 409 fires the
|
|
1686
|
+
// callback above); make the reason visible either way.
|
|
1687
|
+
const stopReason = telegram.getPollStopReason();
|
|
1688
|
+
if (stopReason) {
|
|
1689
|
+
addHarnessMessage("Telegram", `inbound polling stopped: ${stopReason}`, "warn", [
|
|
1690
|
+
"Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
|
|
1691
|
+
]);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1635
1695
|
if (!telegram?.isGreetingVerified()) {
|
|
1636
1696
|
addHarnessMessage("Telegram", "Hi sent; tap Say Hi to verify inbound (tap Toggle Telegram to disable)", "wait");
|
|
1637
1697
|
}
|
|
@@ -1795,8 +1855,15 @@ function reportLineConfiguration(): void {
|
|
|
1795
1855
|
channelState: lineChannelState,
|
|
1796
1856
|
telegramConfigured: telegramIsConfigured(),
|
|
1797
1857
|
// The fallback makes sure that a warning always carries a command,
|
|
1798
|
-
// even when the audit could not answer.
|
|
1799
|
-
|
|
1858
|
+
// even when the audit could not answer. It names the ACCESS TOKEN
|
|
1859
|
+
// only - the user id is the recipient of the messages, not the cause
|
|
1860
|
+
// of a failed startup check.
|
|
1861
|
+
fixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
|
|
1862
|
+
// The startup check validates the access token - the user id is only
|
|
1863
|
+
// the recipient, so it must not be named as the cause.
|
|
1864
|
+
accessTokenFixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
|
|
1865
|
+
// The reason the channel did not start (invalid token, LINE down, ...).
|
|
1866
|
+
channelDiagnostic: lineChannelDiagnostic,
|
|
1800
1867
|
// Without Telegram the LINE channel cannot start - the user needs
|
|
1801
1868
|
// the Telegram commands, not only an explanation.
|
|
1802
1869
|
telegramFixes: keyFileFixesOrFallback("Telegram", ["bot token", "chat id"]),
|
|
@@ -1911,33 +1978,57 @@ async function initRelayStream(): Promise<void> {
|
|
|
1911
1978
|
logToFile("[RelayStream]", "Synchronized - receiving live events");
|
|
1912
1979
|
},
|
|
1913
1980
|
onCallback: async (callback: CallbackPayload) => {
|
|
1914
|
-
logToFile(
|
|
1981
|
+
logToFile(
|
|
1982
|
+
"[RelayStream]",
|
|
1983
|
+
callback.type === "telegram_message"
|
|
1984
|
+
? `Message from ${callback.username || callback.userId} (${(callback.text || "").length} chars)`
|
|
1985
|
+
: `Callback: ${callback.action} ${callback.targetId}`,
|
|
1986
|
+
);
|
|
1915
1987
|
|
|
1916
1988
|
// Forward Telegram callbacks to the adapter
|
|
1917
1989
|
if (callback.type === "telegram_callback" && _relayTelegramAdapter) {
|
|
1918
|
-
//
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1990
|
+
// The mapping is a pure function (harness/telegram-update.ts) so
|
|
1991
|
+
// that it can be tested without the harness.
|
|
1992
|
+
const update = buildTelegramCallbackUpdate({
|
|
1993
|
+
id: callback.id,
|
|
1994
|
+
action: callback.action,
|
|
1995
|
+
targetId: callback.targetId,
|
|
1996
|
+
callbackQueryId: callback.callbackQueryId,
|
|
1997
|
+
userId: callback.userId,
|
|
1998
|
+
username: callback.username,
|
|
1999
|
+
messageId: callback.messageId,
|
|
2000
|
+
chatId: callback.chatId,
|
|
2001
|
+
isBot: callback.isBot,
|
|
2002
|
+
});
|
|
2003
|
+
if (update) {
|
|
2004
|
+
try {
|
|
2005
|
+
await _relayTelegramAdapter.handleWebhookUpdate(update);
|
|
2006
|
+
logToFile("[RelayStream]", "Callback processed by Telegram adapter");
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1935
2012
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2013
|
+
// Forward a Telegram TEXT message (the user typed it) to the adapter
|
|
2014
|
+
if (callback.type === "telegram_message" && _relayTelegramAdapter) {
|
|
2015
|
+
// The mapping is a pure function (harness/telegram-update.ts) so
|
|
2016
|
+
// that it can be tested without the harness.
|
|
2017
|
+
const update = buildTelegramMessageUpdate({
|
|
2018
|
+
userId: callback.userId,
|
|
2019
|
+
username: callback.username,
|
|
2020
|
+
messageId: callback.messageId,
|
|
2021
|
+
chatId: callback.chatId,
|
|
2022
|
+
text: callback.text,
|
|
2023
|
+
isBot: callback.isBot,
|
|
2024
|
+
});
|
|
2025
|
+
if (update) {
|
|
2026
|
+
try {
|
|
2027
|
+
await _relayTelegramAdapter.handleWebhookUpdate(update);
|
|
2028
|
+
logToFile("[RelayStream]", "Message processed by Telegram adapter");
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
|
|
2031
|
+
}
|
|
1941
2032
|
}
|
|
1942
2033
|
}
|
|
1943
2034
|
},
|
|
@@ -2688,8 +2779,10 @@ const HARNESS_LOG_FILE = "/tmp/pi-harness.log";
|
|
|
2688
2779
|
/** Log a harness message to file (not stderr - bypasses Pi's cursor tracking) */
|
|
2689
2780
|
function logToFile(...parts: string[]): void {
|
|
2690
2781
|
const msg = parts.join(" ");
|
|
2782
|
+
// A timestamp makes the log usable for latency measurements.
|
|
2783
|
+
const stamp = new Date().toISOString();
|
|
2691
2784
|
try {
|
|
2692
|
-
appendFileSync(HARNESS_LOG_FILE, msg
|
|
2785
|
+
appendFileSync(HARNESS_LOG_FILE, `[${stamp}] ${msg}\n`);
|
|
2693
2786
|
} catch {
|
|
2694
2787
|
// Ignore file write errors
|
|
2695
2788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.392",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|