appback-remoteagent 0.17.1 → 0.20.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/README.md +33 -0
- package/bin/remoteagent.js +1 -1
- package/dist/adapters/codex-adapter.js +22 -4
- package/dist/bot.js +321 -53
- package/dist/cli.js +208 -0
- package/dist/services/agent-memory-service.js +6 -4
- package/dist/services/bot-management-service.js +3 -0
- package/dist/services/bridge-service.js +16 -7
- package/dist/services/cli-config-service.js +154 -0
- package/dist/services/secret-transfer-service.js +237 -0
- package/dist/telegram-command-menu.js +1 -1
- package/docs/CLI_BOOTSTRAP_AND_SECRET_MIGRATION.md +98 -0
- package/docs/RELEASING.md +22 -0
- package/package.json +2 -1
- package/scripts/install.sh +9 -0
- package/scripts/selftest-cli.mjs +121 -0
- package/scripts/selftest-codex-stream.mjs +9 -2
- package/scripts/selftest-telegram-update.mjs +120 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# CLI bootstrap and secret migration
|
|
2
|
+
|
|
3
|
+
## First Telegram bot
|
|
4
|
+
|
|
5
|
+
Install RemoteAgent, seed its runtime configuration, and register the first Telegram bot:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g appback-remoteagent
|
|
9
|
+
remoteagent-install
|
|
10
|
+
remoteagent bot add
|
|
11
|
+
remoteagent-start
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`remoteagent bot add` asks for the BotFather token without echoing it and then asks for the numeric Telegram owner user ID. It validates the token with Telegram `getMe` before writing `~/.remoteagent/.env`.
|
|
15
|
+
|
|
16
|
+
For automation, keep sensitive values out of shell history by using a permission-restricted token file:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
chmod 600 /secure/path/telegram-token
|
|
20
|
+
remoteagent bot add --token-file /secure/path/telegram-token --owner 123456789
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After adding or updating a bot, apply the configuration with the runtime command appropriate to the installation:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
remoteagent-start
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sudo systemctl restart remoteagent
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Secret migration
|
|
34
|
+
|
|
35
|
+
RemoteAgent `/secret` values belong to the installation, not to an individual agent session. They are stored under `~/.remoteagent/managed/secrets.json`.
|
|
36
|
+
|
|
37
|
+
Export them on the old PC as a password-encrypted bundle:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
remoteagent secret export ~/remoteagent-secrets.ra-secrets
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The command asks twice for a bundle passphrase. The resulting file uses scrypt key derivation and AES-256-GCM authenticated encryption; secret values are never printed.
|
|
44
|
+
|
|
45
|
+
Transfer the encrypted file to the new PC, install RemoteAgent, and import it:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
remoteagent-install
|
|
49
|
+
remoteagent secret import ~/remoteagent-secrets.ra-secrets
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Import merges the bundle into the new PC's installation-wide Secret store. Imported keys replace keys with the same name; unrelated existing keys remain. Before overwriting an existing store, RemoteAgent creates a permission-restricted timestamped backup beside `secrets.json`.
|
|
53
|
+
|
|
54
|
+
Use `--replace` only when the imported bundle must become the entire Secret store:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
remoteagent secret import ~/remoteagent-secrets.ra-secrets --replace
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For non-interactive automation, provide a permission-restricted passphrase file:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
chmod 600 /secure/path/transfer-passphrase
|
|
64
|
+
remoteagent secret export ~/remoteagent-secrets.ra-secrets --passphrase-file /secure/path/transfer-passphrase
|
|
65
|
+
remoteagent secret import ~/remoteagent-secrets.ra-secrets --passphrase-file /secure/path/transfer-passphrase
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The Telegram bot token and `TELEGRAM_OWNER_ID` are runtime configuration, not `/secret` values. Register the bot separately with `remoteagent bot add` on the new PC.
|
|
69
|
+
|
|
70
|
+
## Encrypted delivery through Telegram
|
|
71
|
+
|
|
72
|
+
RemoteAgent can send selected Secret values back to the current private chat without exposing their values to the provider or Telegram message text. First store a transfer passphrase that you know:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
/secret set REMOTEAGENT_TRANSFER_PASSPHRASE a-long-private-passphrase
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then export only the required keys:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
/secret export REMOTEAGENT_TRANSFER_PASSPHRASE APPBACK_RELEASE_STORE_PASSWORD APPBACK_RELEASE_KEY_PASSWORD APPBACK_RELEASE_KEYSTORE_BASE64 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
RemoteAgent performs these steps itself:
|
|
85
|
+
|
|
86
|
+
1. Reads the passphrase and selected Secret values without sending them to Codex or Claude.
|
|
87
|
+
2. Excludes the passphrase key from the bundle.
|
|
88
|
+
3. Compresses the payload with gzip and encrypts it with AES-256-GCM.
|
|
89
|
+
4. Sends the `.ra-secrets` file to the same Telegram chat.
|
|
90
|
+
5. Removes the temporary server-side bundle after delivery.
|
|
91
|
+
|
|
92
|
+
Import the received file on the destination PC with the CLI:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
remoteagent secret import ~/Downloads/remoteagent-secrets-S001-20260811.ra-secrets
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `/secret set` source message is deleted from the private chat after successful storage when Telegram permits deletion, and local RemoteAgent logs redact its value. Telegram bot chats are not end-to-end encrypted, so the local CLI export remains the strongest option for especially sensitive credentials.
|
package/docs/RELEASING.md
CHANGED
|
@@ -220,3 +220,25 @@ npm run selftest:telegram
|
|
|
220
220
|
npm run release:publish
|
|
221
221
|
npm run release:deploy -- 0.17.1 30
|
|
222
222
|
```
|
|
223
|
+
|
|
224
|
+
## Release 0.18.0
|
|
225
|
+
|
|
226
|
+
Date: 2026-08-03
|
|
227
|
+
|
|
228
|
+
Changes:
|
|
229
|
+
|
|
230
|
+
- Telegram list and option responses provide inline command controls.
|
|
231
|
+
- Queue notices provide inline remove-latest and remove-by-id controls in one message.
|
|
232
|
+
- Codex progress streaming accepts both normalized CLI `item.completed` events and raw `event_msg`/`response_item` agent-message events.
|
|
233
|
+
- The Codex stream self-test covers every supported progress event shape and keeps final results out of the progress callback.
|
|
234
|
+
|
|
235
|
+
Validated:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run check
|
|
239
|
+
npm run build
|
|
240
|
+
npm run selftest:codex-stream
|
|
241
|
+
npm run selftest:telegram
|
|
242
|
+
npm run release:publish
|
|
243
|
+
npm run release:deploy -- 0.18.0 30
|
|
244
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appback-remoteagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Personal installable session server for continuing local AI work across PC and Telegram",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
21
21
|
"selftest:telegram": "npm run build && node scripts/selftest-telegram-update.mjs",
|
|
22
22
|
"selftest:codex-stream": "npm run build && node scripts/selftest-codex-stream.mjs",
|
|
23
|
+
"selftest:cli": "npm run build && node scripts/selftest-cli.mjs",
|
|
23
24
|
"prepare": "npm run build",
|
|
24
25
|
"prepublishOnly": "node scripts/prepublish-guard.mjs",
|
|
25
26
|
"release:version": "bash scripts/release-version.sh",
|
package/scripts/install.sh
CHANGED
|
@@ -45,6 +45,13 @@ fi
|
|
|
45
45
|
|
|
46
46
|
mkdir -p "$DATA_DIR" "$DATA_DIR/logs"
|
|
47
47
|
|
|
48
|
+
NODE_BIN_PATH="$(command -v node || true)"
|
|
49
|
+
if [ -z "$NODE_BIN_PATH" ]; then
|
|
50
|
+
echo "node is required to install RemoteAgent." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
NODE_BIN_DIR="$(cd -P "$(dirname "$NODE_BIN_PATH")" && pwd)"
|
|
54
|
+
|
|
48
55
|
if [ ! -f "$ENV_FILE" ]; then
|
|
49
56
|
cp "$ROOT_DIR/.env.example" "$ENV_FILE"
|
|
50
57
|
echo "Created $ENV_FILE"
|
|
@@ -84,6 +91,8 @@ upsert_env "BOT_RESTART_HELPER_PATH" "$ROOT_DIR/scripts/restart-after-bot-op.sh"
|
|
|
84
91
|
|
|
85
92
|
cat > "$DATA_DIR/start-remoteagent.sh" <<EOF
|
|
86
93
|
#!/usr/bin/env bash
|
|
94
|
+
export PATH="$NODE_BIN_DIR:\$PATH"
|
|
95
|
+
export NODE_BIN="$NODE_BIN_PATH"
|
|
87
96
|
DATA_DIR="$DATA_DIR" "$ROOT_DIR/scripts/start.sh"
|
|
88
97
|
EOF
|
|
89
98
|
chmod +x "$DATA_DIR/start-remoteagent.sh"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { registerTelegramBot } from "../dist/services/cli-config-service.js";
|
|
8
|
+
import { exportSecrets, importSecrets } from "../dist/services/secret-transfer-service.js";
|
|
9
|
+
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-cli-selftest-"));
|
|
11
|
+
const sourceDataDir = path.join(root, "source");
|
|
12
|
+
const targetDataDir = path.join(root, "target");
|
|
13
|
+
const bundlePath = path.join(root, "transfer.ra-secrets");
|
|
14
|
+
const selectedBundlePath = path.join(root, "selected-transfer.ra-secrets");
|
|
15
|
+
const passphrase = "correct-horse-battery-staple";
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await fs.mkdir(sourceDataDir, { recursive: true });
|
|
19
|
+
await fs.writeFile(path.join(sourceDataDir, ".env"), [
|
|
20
|
+
"TELEGRAM_BOT_TOKEN=your-telegram-bot-token",
|
|
21
|
+
"TELEGRAM_BOT_TOKENS=",
|
|
22
|
+
"TELEGRAM_OWNER_ID=",
|
|
23
|
+
"DEFAULT_MODE=codex",
|
|
24
|
+
"",
|
|
25
|
+
].join("\n"), { mode: 0o600 });
|
|
26
|
+
|
|
27
|
+
const first = await registerTelegramBot({
|
|
28
|
+
dataDir: sourceDataDir,
|
|
29
|
+
token: "100001:abcdefghijklmnopqrstuvwxyz_123456",
|
|
30
|
+
ownerId: "8202993989",
|
|
31
|
+
identity: { id: 100001, username: "first_remoteagent_bot" },
|
|
32
|
+
});
|
|
33
|
+
assert.equal(first.added, true);
|
|
34
|
+
assert.equal(first.botCount, 1);
|
|
35
|
+
|
|
36
|
+
const second = await registerTelegramBot({
|
|
37
|
+
dataDir: sourceDataDir,
|
|
38
|
+
token: "100002:abcdefghijklmnopqrstuvwxyz_654321",
|
|
39
|
+
ownerId: "8202993989",
|
|
40
|
+
identity: { id: 100002, username: "second_remoteagent_bot" },
|
|
41
|
+
});
|
|
42
|
+
assert.equal(second.botCount, 2);
|
|
43
|
+
const envText = await fs.readFile(path.join(sourceDataDir, ".env"), "utf8");
|
|
44
|
+
assert.match(envText, /DEFAULT_MODE=codex/);
|
|
45
|
+
assert.match(envText, /TELEGRAM_OWNER_ID=8202993989/);
|
|
46
|
+
assert.doesNotMatch(envText, /your-telegram-bot-token/);
|
|
47
|
+
assert.match(envText, /TELEGRAM_BOT_USERNAMES=first_remoteagent_bot,second_remoteagent_bot/);
|
|
48
|
+
|
|
49
|
+
const sourceSecrets = {
|
|
50
|
+
API_TOKEN: {
|
|
51
|
+
key: "API_TOKEN",
|
|
52
|
+
value: "plain-value-must-not-appear-in-bundle",
|
|
53
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
54
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
55
|
+
},
|
|
56
|
+
DB_PASSWORD: {
|
|
57
|
+
key: "DB_PASSWORD",
|
|
58
|
+
value: "another-private-value",
|
|
59
|
+
createdAt: "2026-01-02T00:00:00.000Z",
|
|
60
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
await fs.mkdir(path.join(sourceDataDir, "managed"), { recursive: true });
|
|
64
|
+
await fs.writeFile(
|
|
65
|
+
path.join(sourceDataDir, "managed", "secrets.json"),
|
|
66
|
+
`${JSON.stringify(sourceSecrets, null, 2)}\n`,
|
|
67
|
+
{ mode: 0o600 },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const exported = await exportSecrets(sourceDataDir, bundlePath, passphrase);
|
|
71
|
+
assert.equal(exported.count, 2);
|
|
72
|
+
const bundleText = await fs.readFile(bundlePath, "utf8");
|
|
73
|
+
assert.doesNotMatch(bundleText, /plain-value-must-not-appear-in-bundle/);
|
|
74
|
+
assert.doesNotMatch(bundleText, /another-private-value/);
|
|
75
|
+
assert.match(bundleText, /remoteagent-secret-bundle/);
|
|
76
|
+
assert.match(bundleText, /"compression": "gzip"/);
|
|
77
|
+
|
|
78
|
+
const selectedExport = await exportSecrets(sourceDataDir, selectedBundlePath, passphrase, {
|
|
79
|
+
includeKeys: ["API_TOKEN"],
|
|
80
|
+
});
|
|
81
|
+
assert.equal(selectedExport.count, 1);
|
|
82
|
+
const selectedDataDir = path.join(root, "selected-target");
|
|
83
|
+
await importSecrets(selectedDataDir, selectedBundlePath, passphrase);
|
|
84
|
+
const selectedSecrets = JSON.parse(await fs.readFile(path.join(selectedDataDir, "managed", "secrets.json"), "utf8"));
|
|
85
|
+
assert.deepEqual(Object.keys(selectedSecrets), ["API_TOKEN"]);
|
|
86
|
+
|
|
87
|
+
await fs.mkdir(path.join(targetDataDir, "managed"), { recursive: true });
|
|
88
|
+
await fs.writeFile(path.join(targetDataDir, "managed", "secrets.json"), JSON.stringify({
|
|
89
|
+
API_TOKEN: {
|
|
90
|
+
key: "API_TOKEN",
|
|
91
|
+
value: "old-value",
|
|
92
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
93
|
+
updatedAt: "2025-01-01T00:00:00.000Z",
|
|
94
|
+
},
|
|
95
|
+
KEEP_ME: {
|
|
96
|
+
key: "KEEP_ME",
|
|
97
|
+
value: "kept-value",
|
|
98
|
+
createdAt: "2025-01-01T00:00:00.000Z",
|
|
99
|
+
updatedAt: "2025-01-01T00:00:00.000Z",
|
|
100
|
+
},
|
|
101
|
+
}, null, 2), { mode: 0o600 });
|
|
102
|
+
|
|
103
|
+
const imported = await importSecrets(targetDataDir, bundlePath, passphrase);
|
|
104
|
+
assert.equal(imported.imported, 2);
|
|
105
|
+
assert.equal(imported.overwritten, 1);
|
|
106
|
+
assert.equal(imported.total, 3);
|
|
107
|
+
assert.ok(imported.backupPath);
|
|
108
|
+
const importedSecrets = JSON.parse(await fs.readFile(path.join(targetDataDir, "managed", "secrets.json"), "utf8"));
|
|
109
|
+
assert.equal(importedSecrets.API_TOKEN.value, sourceSecrets.API_TOKEN.value);
|
|
110
|
+
assert.equal(importedSecrets.DB_PASSWORD.value, sourceSecrets.DB_PASSWORD.value);
|
|
111
|
+
assert.equal(importedSecrets.KEEP_ME.value, "kept-value");
|
|
112
|
+
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
importSecrets(path.join(root, "wrong-passphrase"), bundlePath, "wrong-passphrase"),
|
|
115
|
+
/could not be decrypted/,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
console.log("RemoteAgent CLI self-test passed.");
|
|
119
|
+
} finally {
|
|
120
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
121
|
+
}
|
|
@@ -23,7 +23,9 @@ printf '%s\\n' '{"type":"thread.started","thread_id":"stream-thread"}'
|
|
|
23
23
|
printf '%s' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:progress\\nphase one"}}'
|
|
24
24
|
printf '\\n'
|
|
25
25
|
sleep 0.05
|
|
26
|
-
printf '%s\\n' '{"type":"
|
|
26
|
+
printf '%s\\n' '{"type":"event_msg","payload":{"type":"agent_message","message":"REPORT:progress\\nphase two"}}'
|
|
27
|
+
sleep 0.05
|
|
28
|
+
printf '%s\\n' '{"type":"response_item","payload":{"type":"message","content":[{"type":"output_text","text":"REPORT:progress\\nphase three"}]}}'
|
|
27
29
|
sleep 0.05
|
|
28
30
|
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:result\\nfinished"}}'
|
|
29
31
|
printf '%s\\n' 'REPORT:result' 'finished' > "$output"
|
|
@@ -50,7 +52,12 @@ const responsePromise = adapter.send({
|
|
|
50
52
|
const response = await responsePromise;
|
|
51
53
|
settled = true;
|
|
52
54
|
|
|
53
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
progress.length !== 3
|
|
57
|
+
|| !progress[0]?.includes("phase one")
|
|
58
|
+
|| !progress[1]?.includes("phase two")
|
|
59
|
+
|| !progress[2]?.includes("phase three")
|
|
60
|
+
) {
|
|
54
61
|
throw new Error(`Unexpected streamed progress: ${JSON.stringify(progress)}`);
|
|
55
62
|
}
|
|
56
63
|
if (progress.some((item) => item.includes("REPORT:result"))) {
|
|
@@ -11,6 +11,7 @@ const workspace = path.join(tmp, "workspace");
|
|
|
11
11
|
const workspaceRoot = path.join(tmp, "workspaces");
|
|
12
12
|
const binDir = path.join(tmp, "bin");
|
|
13
13
|
const telegramCalls = path.join(tmp, "telegram-calls.jsonl");
|
|
14
|
+
const capturedDocument = path.join(tmp, "captured-document.ra-secrets");
|
|
14
15
|
|
|
15
16
|
await fs.mkdir(workspace, { recursive: true });
|
|
16
17
|
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
@@ -22,6 +23,7 @@ method="unknown"
|
|
|
22
23
|
text=""
|
|
23
24
|
chat_id=""
|
|
24
25
|
reply_markup=""
|
|
26
|
+
document_path=""
|
|
25
27
|
for arg in "$@"; do
|
|
26
28
|
case "$arg" in
|
|
27
29
|
https://api.telegram.org/bot*/sendMessage) method="sendMessage" ;;
|
|
@@ -32,6 +34,7 @@ for arg in "$@"; do
|
|
|
32
34
|
chat_id=*) chat_id="\${arg#chat_id=}" ;;
|
|
33
35
|
text=*) text="\${arg#text=}" ;;
|
|
34
36
|
reply_markup=*) reply_markup="\${arg#reply_markup=}" ;;
|
|
37
|
+
document=@*) document_path="\${arg#document=@}" ;;
|
|
35
38
|
esac
|
|
36
39
|
done
|
|
37
40
|
text_b64="$(printf '%s' "$text" | base64 -w 0)"
|
|
@@ -45,6 +48,7 @@ case "$method" in
|
|
|
45
48
|
printf '{"ok":true,"result":true}'
|
|
46
49
|
;;
|
|
47
50
|
sendDocument)
|
|
51
|
+
cp "$document_path" ${JSON.stringify(capturedDocument)}
|
|
48
52
|
printf '{"ok":true,"result":{"message_id":1002,"document":{"file_id":"fake"}}}'
|
|
49
53
|
;;
|
|
50
54
|
*)
|
|
@@ -72,6 +76,7 @@ const [
|
|
|
72
76
|
{ BotManagementService },
|
|
73
77
|
{ FileStore },
|
|
74
78
|
{ AgentMemoryService },
|
|
79
|
+
{ importSecrets },
|
|
75
80
|
{ WorkspaceCleanupService },
|
|
76
81
|
{ buildFallbackBotInfo },
|
|
77
82
|
] = await Promise.all([
|
|
@@ -80,6 +85,7 @@ const [
|
|
|
80
85
|
import(path.join(root, "dist", "services", "bot-management-service.js")),
|
|
81
86
|
import(path.join(root, "dist", "store", "file-store.js")),
|
|
82
87
|
import(path.join(root, "dist", "services", "agent-memory-service.js")),
|
|
88
|
+
import(path.join(root, "dist", "services", "secret-transfer-service.js")),
|
|
83
89
|
import(path.join(root, "dist", "services", "workspace-cleanup-service.js")),
|
|
84
90
|
import(path.join(root, "dist", "telegram-bot-identity.js")),
|
|
85
91
|
]);
|
|
@@ -254,6 +260,14 @@ async function click(data) {
|
|
|
254
260
|
await injectedBot.handleUpdates([callbackUpdate(data)]);
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
function findInlineButton(call, label) {
|
|
264
|
+
if (!call?.reply_markup) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const markup = JSON.parse(call.reply_markup);
|
|
268
|
+
return markup.inline_keyboard?.flat().find((button) => button.text === label);
|
|
269
|
+
}
|
|
270
|
+
|
|
257
271
|
async function readTelegramCalls() {
|
|
258
272
|
return (await fs.readFile(telegramCalls, "utf8"))
|
|
259
273
|
.trim()
|
|
@@ -287,6 +301,9 @@ await send("/start codex");
|
|
|
287
301
|
await send("/option retry 6");
|
|
288
302
|
await send("/option timeout 600");
|
|
289
303
|
await send("/option intent 4");
|
|
304
|
+
await send("/secret set REMOTEAGENT_TRANSFER_PASSPHRASE correct-horse-battery-staple");
|
|
305
|
+
await send("/secret set API_TOKEN telegram-secret-export-value");
|
|
306
|
+
await send("/secret export REMOTEAGENT_TRANSFER_PASSPHRASE API_TOKEN");
|
|
290
307
|
await send("같은 값을 봐야하는데 로직문제네? 확인해줘\\n이미 수정되어 있을 수 있어.\\n나한테 수정했다고 보고했었거든");
|
|
291
308
|
await send("/state");
|
|
292
309
|
|
|
@@ -310,6 +327,32 @@ if (!/^TELEGRAM_UNTAGGED_INTENT_RETRIES=4$/m.test(envText)) {
|
|
|
310
327
|
throw new Error(`Option command did not persist untagged intent retry limit to .env: ${envText}`);
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
const importedSecretDataDir = path.join(tmp, "imported-secret-data");
|
|
331
|
+
const importedSecretResult = await importSecrets(
|
|
332
|
+
importedSecretDataDir,
|
|
333
|
+
capturedDocument,
|
|
334
|
+
"correct-horse-battery-staple",
|
|
335
|
+
);
|
|
336
|
+
if (importedSecretResult.imported !== 1) {
|
|
337
|
+
throw new Error(`Expected one Telegram-exported Secret, got ${importedSecretResult.imported}`);
|
|
338
|
+
}
|
|
339
|
+
const importedSecretStore = JSON.parse(
|
|
340
|
+
await fs.readFile(path.join(importedSecretDataDir, "managed", "secrets.json"), "utf8"),
|
|
341
|
+
);
|
|
342
|
+
if (importedSecretStore.API_TOKEN?.value !== "telegram-secret-export-value") {
|
|
343
|
+
throw new Error("Telegram Secret export did not preserve the selected Secret value");
|
|
344
|
+
}
|
|
345
|
+
if (importedSecretStore.REMOTEAGENT_TRANSFER_PASSPHRASE) {
|
|
346
|
+
throw new Error("Telegram Secret export included its transfer passphrase key");
|
|
347
|
+
}
|
|
348
|
+
const secretTelegramCalls = await readTelegramCalls();
|
|
349
|
+
if (secretTelegramCalls.filter((call) => call.method === "deleteMessage").length < 2) {
|
|
350
|
+
throw new Error("Secret source messages were not deleted after storage");
|
|
351
|
+
}
|
|
352
|
+
if (!secretTelegramCalls.some((call) => call.method === "sendDocument")) {
|
|
353
|
+
throw new Error("Encrypted Secret bundle was not sent as a Telegram document");
|
|
354
|
+
}
|
|
355
|
+
|
|
313
356
|
const sessionWorkspace = session.workspace;
|
|
314
357
|
await fs.mkdir(path.join(sessionWorkspace, "node_modules", "left-pad"), { recursive: true });
|
|
315
358
|
await fs.mkdir(path.join(sessionWorkspace, "src"), { recursive: true });
|
|
@@ -440,6 +483,83 @@ if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(ca
|
|
|
440
483
|
throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
441
484
|
}
|
|
442
485
|
|
|
486
|
+
await send("/new");
|
|
487
|
+
await send("/list");
|
|
488
|
+
const sessionListCall = await waitForTelegramCall((call) => call.text.includes("Sessions (2/2)"));
|
|
489
|
+
const firstSessionButton = findInlineButton(sessionListCall, `S001 · ${path.basename(session.workspace)}`);
|
|
490
|
+
if (!firstSessionButton?.callback_data?.startsWith("remoteagent:action:")) {
|
|
491
|
+
throw new Error(`Session switch button is missing: ${sessionListCall.reply_markup}`);
|
|
492
|
+
}
|
|
493
|
+
await click(firstSessionButton.callback_data);
|
|
494
|
+
await waitForTelegramCall((call) => call.text.includes("Switched this chat to session S001."));
|
|
495
|
+
|
|
496
|
+
await send("/model");
|
|
497
|
+
const modelListCall = await waitForTelegramCall((call) => call.text.includes("availablePresets:"));
|
|
498
|
+
const modelButton = findInlineButton(modelListCall, "gpt-5.6-terra");
|
|
499
|
+
if (!modelButton?.callback_data) {
|
|
500
|
+
throw new Error(`Model selection button is missing: ${modelListCall.reply_markup}`);
|
|
501
|
+
}
|
|
502
|
+
await click(modelButton.callback_data);
|
|
503
|
+
await waitForTelegramCall((call) => call.text.includes("Set codex model to gpt-5.6-terra."));
|
|
504
|
+
const modelState = JSON.parse(await fs.readFile(path.join(dataDir, "state.json"), "utf8"));
|
|
505
|
+
if (modelState.sessions[session.sessionId]?.codex?.model !== "gpt-5.6-terra") {
|
|
506
|
+
throw new Error("Model button did not update the bound session model");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
await send("/option");
|
|
510
|
+
const optionListCall = await waitForTelegramCall((call) => call.text.startsWith("Runtime options"));
|
|
511
|
+
const timeoutButton = findInlineButton(optionListCall, "Timeout");
|
|
512
|
+
if (!timeoutButton?.callback_data) {
|
|
513
|
+
throw new Error(`Runtime option button is missing: ${optionListCall.reply_markup}`);
|
|
514
|
+
}
|
|
515
|
+
await click(timeoutButton.callback_data);
|
|
516
|
+
await waitForTelegramCall((call) => call.text.includes("Current provider execution timeout: 600s"));
|
|
517
|
+
|
|
518
|
+
await send("/sandbox");
|
|
519
|
+
const sandboxListCall = await waitForTelegramCall((call) => call.text.startsWith("Codex sandbox"));
|
|
520
|
+
const readOnlyButton = findInlineButton(sandboxListCall, "read-only");
|
|
521
|
+
const dangerButton = findInlineButton(sandboxListCall, "danger-full-access");
|
|
522
|
+
if (!readOnlyButton?.callback_data || !dangerButton?.callback_data) {
|
|
523
|
+
throw new Error(`Sandbox selection buttons are missing: ${sandboxListCall.reply_markup}`);
|
|
524
|
+
}
|
|
525
|
+
await click(readOnlyButton.callback_data);
|
|
526
|
+
await waitForTelegramCall((call) => call.text.includes("Set Codex sandbox to read-only."));
|
|
527
|
+
await click(dangerButton.callback_data);
|
|
528
|
+
const sandboxConfirmCall = await waitForTelegramCall((call) => call.text.includes("Confirm Codex sandbox change"));
|
|
529
|
+
if (!findInlineButton(sandboxConfirmCall, "Confirm danger-full-access")?.callback_data) {
|
|
530
|
+
throw new Error(`Danger sandbox confirmation button is missing: ${sandboxConfirmCall.reply_markup}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
await send("/macro set button-test inspect the callback path");
|
|
534
|
+
await send("/batch start");
|
|
535
|
+
await send("/macro");
|
|
536
|
+
const macroListCall = await waitForTelegramCall((call) => call.text.includes("Macros (1)"));
|
|
537
|
+
const macroButton = findInlineButton(macroListCall, "button-test");
|
|
538
|
+
if (!macroButton?.callback_data) {
|
|
539
|
+
throw new Error(`Macro execution button is missing: ${macroListCall.reply_markup}`);
|
|
540
|
+
}
|
|
541
|
+
await click(macroButton.callback_data);
|
|
542
|
+
await send("/batch send");
|
|
543
|
+
await waitForTelegramCall((call) => call.text.includes("mock provider completed"));
|
|
544
|
+
|
|
545
|
+
await fs.appendFile(path.join(dataDir, ".env"), [
|
|
546
|
+
"TELEGRAM_BOT_TOKENS=000000:test-token",
|
|
547
|
+
"TELEGRAM_BOT_USERNAMES=remoteagent_test_bot",
|
|
548
|
+
"",
|
|
549
|
+
].join("\n"), "utf8");
|
|
550
|
+
await send("/bots");
|
|
551
|
+
const botsCall = await waitForTelegramCall((call) => call.text.includes("Configured bots (1)"));
|
|
552
|
+
const botLink = findInlineButton(botsCall, "@bot_0");
|
|
553
|
+
const refreshButton = findInlineButton(botsCall, "Refresh");
|
|
554
|
+
if (botLink?.url !== "https://t.me/bot_0" || !refreshButton?.callback_data) {
|
|
555
|
+
throw new Error(`Bot link or refresh button is missing: ${botsCall.reply_markup}`);
|
|
556
|
+
}
|
|
557
|
+
await click(refreshButton.callback_data);
|
|
558
|
+
const refreshedBotsCalls = (await readTelegramCalls()).filter((call) => call.text.includes("Configured bots (1)"));
|
|
559
|
+
if (refreshedBotsCalls.length < 2) {
|
|
560
|
+
throw new Error("Bot refresh callback did not render the bot list again");
|
|
561
|
+
}
|
|
562
|
+
|
|
443
563
|
providerMode = "timeout";
|
|
444
564
|
await send("/batch start");
|
|
445
565
|
await send("timeout regression test");
|