polygram 0.38.2 → 0.38.4
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/.claude-plugin/plugin.json +1 -1
- package/lib/approvals/store.js +45 -7
- package/lib/codex/runtime-controller.js +107 -1
- package/lib/db/event-detail-schema.js +524 -0
- package/lib/db/pairings.js +9 -1
- package/lib/db/secret-sweep.js +6 -3
- package/lib/db/sessions.js +2 -2
- package/lib/db.js +308 -49
- package/lib/handlers/abort.js +20 -3
- package/lib/handlers/approvals.js +2 -2
- package/lib/handlers/config-callback.js +7 -7
- package/lib/handlers/dispatcher.js +11 -9
- package/lib/handlers/questions.js +546 -45
- package/lib/handlers/slash-commands.js +54 -13
- package/lib/handlers/voice.js +1 -1
- package/lib/ops/clean-shutdown.js +5 -0
- package/lib/questions/questions.js +51 -0
- package/lib/questions/store.js +61 -5
- package/lib/rewind/execute.js +12 -2
- package/lib/rewind/rewind.js +1 -1
- package/lib/sdk/build-options.js +2 -2
- package/lib/sdk/callbacks.js +14 -9
- package/lib/secret-detect.js +280 -12
- package/lib/telegram/api.js +10 -2
- package/lib/telegram/rich-edit.js +9 -5
- package/lib/telegram/rich-send.js +6 -6
- package/migrations/020-drop-secret-fingerprint.sql +13 -0
- package/package.json +2 -2
- package/polygram.js +82 -45
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.38.
|
|
4
|
+
"version": "0.38.4",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
package/lib/approvals/store.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
const crypto = require('crypto');
|
|
14
14
|
const { canonicalizeToolInput } = require('../canonical-json');
|
|
15
|
+
const {
|
|
16
|
+
sanitizeDurableStructured,
|
|
17
|
+
sanitizeDurableJsonText,
|
|
18
|
+
} = require('../secret-detect');
|
|
15
19
|
|
|
16
20
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
17
21
|
// 16 random bytes → 22 base64url chars ≈ 128 bits of entropy. Prevents
|
|
@@ -164,10 +168,41 @@ function createStore(rawDb, now = () => Date.now()) {
|
|
|
164
168
|
if (!approver_chat_id) throw new Error('approver_chat_id required');
|
|
165
169
|
if (!tool_name) throw new Error('tool_name required');
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
// The row is an audit + display copy: the card renders it and the digest
|
|
172
|
+
// dedups repeated hook fires. The tool itself runs on the caller's own
|
|
173
|
+
// in-memory input, so masking recognized credentials here removes them
|
|
174
|
+
// from durable storage without touching what gets executed. Digesting
|
|
175
|
+
// the masked form also stops the digest from being a fingerprint of the
|
|
176
|
+
// credential, while staying deterministic for dedupe.
|
|
177
|
+
// Sanitize the STRUCTURE before serializing: a value the serializer
|
|
178
|
+
// escapes no longer looks like a declared credential to the detector,
|
|
179
|
+
// masking the serialized form can splice across its delimiters, and a
|
|
180
|
+
// credential named only by its key (`{"password":"…"}`) is invisible to
|
|
181
|
+
// a text pass. A caller that hands over an already-serialized document
|
|
182
|
+
// gets the same treatment — it is parsed back first, and only genuinely
|
|
183
|
+
// non-JSON input falls back to text masking.
|
|
184
|
+
let tool_input_json;
|
|
185
|
+
let sanitized;
|
|
186
|
+
if (typeof tool_input === 'string') {
|
|
187
|
+
const res = sanitizeDurableJsonText(tool_input);
|
|
188
|
+
tool_input_json = res.text;
|
|
189
|
+
sanitized = res.changed;
|
|
190
|
+
} else {
|
|
191
|
+
const source = tool_input || {};
|
|
192
|
+
tool_input_json = JSON.stringify(sanitizeDurableStructured(source));
|
|
193
|
+
sanitized = tool_input_json !== JSON.stringify(source);
|
|
194
|
+
}
|
|
195
|
+
// The legacy dedupe key is (turn, digest of the stored input), which is
|
|
196
|
+
// only sound while the stored input distinguishes two calls. Once a
|
|
197
|
+
// credential is masked it no longer does: two different secrets mask to
|
|
198
|
+
// the same text, so a shared digest would let one operator decision
|
|
199
|
+
// authorize a different command. A sanitized row therefore gets a
|
|
200
|
+
// random per-row identity — unique, and derived from nothing — and skips
|
|
201
|
+
// legacy reuse entirely. Dedupe by the provider's stable tool_use_id is
|
|
202
|
+
// unaffected: that key never depended on the input.
|
|
203
|
+
const tool_input_digest = sanitized
|
|
204
|
+
? crypto.randomBytes(8).toString('hex')
|
|
205
|
+
: digestInput(tool_input_json);
|
|
171
206
|
const requested_ts = now();
|
|
172
207
|
const timeout_ts = requested_ts + timeoutMs;
|
|
173
208
|
const callback_token = newToken();
|
|
@@ -186,9 +221,12 @@ function createStore(rawDb, now = () => Date.now()) {
|
|
|
186
221
|
// which existing row counts as a "match."
|
|
187
222
|
let row, reused = false;
|
|
188
223
|
const tx = rawDb.transaction(() => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
224
|
+
let existing = null;
|
|
225
|
+
if (tool_use_id) {
|
|
226
|
+
existing = findDedupByToolUseIdStmt.get(bot_name, tool_use_id);
|
|
227
|
+
} else if (!sanitized) {
|
|
228
|
+
existing = findDedupStmt.get(bot_name, turn_id, tool_input_digest);
|
|
229
|
+
}
|
|
192
230
|
if (existing) { row = existing; reused = true; return; }
|
|
193
231
|
const res = insertStmt.run({
|
|
194
232
|
bot_name,
|
|
@@ -222,6 +222,7 @@ function createCodexRuntimeController({
|
|
|
222
222
|
|| typeof db.settleCodexQueuedDispatch !== 'function'
|
|
223
223
|
|| typeof db.settleCodexStoppedGeneration !== 'function'
|
|
224
224
|
|| typeof db.settleCodexFailedGeneration !== 'function'
|
|
225
|
+
|| typeof db.prepareCodexCleanRetirement !== 'function'
|
|
225
226
|
|| typeof db.recordCodexDeliveryCheckpoint !== 'function'
|
|
226
227
|
|| typeof resolveHostIdentity !== 'function'
|
|
227
228
|
|| typeof resolveBinary !== 'function'
|
|
@@ -416,6 +417,7 @@ function createCodexRuntimeController({
|
|
|
416
417
|
codexHostIdentity: identity.stableHostId,
|
|
417
418
|
codexBootSessionIdentity: identity.bootSessionId,
|
|
418
419
|
codexRecoveryState,
|
|
420
|
+
codexRetirementPreparer: prepareCodexRetirement,
|
|
419
421
|
codexRetirementVerifier: verifyCodexRetirement,
|
|
420
422
|
}),
|
|
421
423
|
});
|
|
@@ -1137,7 +1139,11 @@ function createCodexRuntimeController({
|
|
|
1137
1139
|
checkpoint,
|
|
1138
1140
|
retireGeneration,
|
|
1139
1141
|
});
|
|
1140
|
-
if (
|
|
1142
|
+
if (
|
|
1143
|
+
retireGeneration
|
|
1144
|
+
&& persisted.retired !== true
|
|
1145
|
+
&& persisted.deferred !== true
|
|
1146
|
+
) {
|
|
1141
1147
|
throw controllerError(
|
|
1142
1148
|
'Codex delivery did not retire the verified stopped generation.',
|
|
1143
1149
|
'CODEX_RETIREMENT_UNVERIFIED',
|
|
@@ -1239,6 +1245,106 @@ function createCodexRuntimeController({
|
|
|
1239
1245
|
}
|
|
1240
1246
|
}
|
|
1241
1247
|
|
|
1248
|
+
function prepareCodexRetirement(input = {}) {
|
|
1249
|
+
const expectedKeys = [
|
|
1250
|
+
'sessionKey',
|
|
1251
|
+
'generationId',
|
|
1252
|
+
'attemptId',
|
|
1253
|
+
'providerSessionId',
|
|
1254
|
+
'providerTurnId',
|
|
1255
|
+
'sourceMsgId',
|
|
1256
|
+
];
|
|
1257
|
+
const keys = input && typeof input === 'object'
|
|
1258
|
+
? Reflect.ownKeys(input)
|
|
1259
|
+
: [];
|
|
1260
|
+
const reject = (message, cause) => {
|
|
1261
|
+
throw controllerError(
|
|
1262
|
+
message,
|
|
1263
|
+
'CODEX_RETIREMENT_PREPARATION_REJECTED',
|
|
1264
|
+
'Do not interrupt or continue the unproven Codex turn.',
|
|
1265
|
+
cause,
|
|
1266
|
+
);
|
|
1267
|
+
};
|
|
1268
|
+
if (
|
|
1269
|
+
keys.length !== expectedKeys.length
|
|
1270
|
+
|| expectedKeys.some((key) => !Object.hasOwn(input, key))
|
|
1271
|
+
) {
|
|
1272
|
+
reject('Codex retirement preparation binding is incomplete.');
|
|
1273
|
+
}
|
|
1274
|
+
for (const key of expectedKeys.slice(0, 5)) {
|
|
1275
|
+
if (
|
|
1276
|
+
typeof input[key] !== 'string'
|
|
1277
|
+
|| input[key].length === 0
|
|
1278
|
+
|| input[key].length > 512
|
|
1279
|
+
) {
|
|
1280
|
+
reject('Codex retirement preparation binding is malformed.');
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (!(
|
|
1284
|
+
(
|
|
1285
|
+
typeof input.sourceMsgId === 'string'
|
|
1286
|
+
&& input.sourceMsgId.length > 0
|
|
1287
|
+
&& input.sourceMsgId.length <= 512
|
|
1288
|
+
)
|
|
1289
|
+
|| (
|
|
1290
|
+
Number.isSafeInteger(input.sourceMsgId)
|
|
1291
|
+
&& input.sourceMsgId > 0
|
|
1292
|
+
)
|
|
1293
|
+
)) {
|
|
1294
|
+
reject('Codex retirement source message binding is malformed.');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const record = processesByGeneration.get(input.generationId);
|
|
1298
|
+
if (
|
|
1299
|
+
!record
|
|
1300
|
+
|| currentProcessBySession.get(input.sessionKey) !== record
|
|
1301
|
+
|| record.proc.sessionKey !== input.sessionKey
|
|
1302
|
+
|| record.proc.generationId !== input.generationId
|
|
1303
|
+
|| record.proc.providerSessionId !== input.providerSessionId
|
|
1304
|
+
|| !record.durable
|
|
1305
|
+
|| record.durabilityBlocked
|
|
1306
|
+
|| record.retired
|
|
1307
|
+
|| !(
|
|
1308
|
+
record.proc.runtime === 'codex'
|
|
1309
|
+
|| record.proc.backend === 'codex'
|
|
1310
|
+
)
|
|
1311
|
+
) {
|
|
1312
|
+
reject('Codex retirement preparation does not own the exact live turn.');
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const state = requireAvailableInitialization();
|
|
1316
|
+
let result;
|
|
1317
|
+
try {
|
|
1318
|
+
result = db.prepareCodexCleanRetirement({
|
|
1319
|
+
generation_id: input.generationId,
|
|
1320
|
+
session_key: input.sessionKey,
|
|
1321
|
+
attempt_id: input.attemptId,
|
|
1322
|
+
provider_session_id: input.providerSessionId,
|
|
1323
|
+
provider_turn_id: input.providerTurnId,
|
|
1324
|
+
source_message_id: String(input.sourceMsgId),
|
|
1325
|
+
stable_host_id: state.identity.stableHostId,
|
|
1326
|
+
boot_session_id: state.identity.bootSessionId,
|
|
1327
|
+
ts: Date.now(),
|
|
1328
|
+
});
|
|
1329
|
+
} catch (cause) {
|
|
1330
|
+
reject('Codex retirement preparation was not committed.', cause);
|
|
1331
|
+
}
|
|
1332
|
+
if (
|
|
1333
|
+
!result
|
|
1334
|
+
|| ![0, 1].includes(result.changes)
|
|
1335
|
+
|| result.disposition !== 'retirement-requested'
|
|
1336
|
+
|| result.generationId !== input.generationId
|
|
1337
|
+
|| result.attemptId !== input.attemptId
|
|
1338
|
+
) {
|
|
1339
|
+
reject('Codex retirement preparation acknowledgement is inexact.');
|
|
1340
|
+
}
|
|
1341
|
+
return Object.freeze({
|
|
1342
|
+
committed: true,
|
|
1343
|
+
disposition: 'retirement-requested',
|
|
1344
|
+
...input,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1242
1348
|
async function verifyCodexRetirement({
|
|
1243
1349
|
sessionKey,
|
|
1244
1350
|
generationId,
|