polygram 0.38.7 → 0.38.9

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.
@@ -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.7",
4
+ "version": "0.38.9",
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",
@@ -69,6 +69,7 @@
69
69
 
70
70
  "codex": {
71
71
  "_comment": "Optional native macOS arm64 or Linux x64 Codex app-server beta. Codex is default-off: a chat/topic must explicitly set codexEnabled:true before pm:'codex' can be selected or dispatched, and exact target-specific startup preflight must succeed; other host targets, including Windows, are rejected. `home` must be a dedicated non-temporary 0700 CODEX_HOME already authenticated with ChatGPT; polygram never copies or reads credential contents. `binary` must be the canonical immutable path to the reviewed target's pinned Codex CLI, not a PATH symlink. On Linux set POLYGRAM_CODEX_TMPDIR to an owner-only 0700 runtime directory; otherwise Codex inherits the daemon TMPDIR after the same safety checks. Every directory containing daemon credentials or control data must be listed in daemonSecretRoots and must not overlap a Codex workspace or CODEX_HOME. Command network and model-native web search are disabled; product MCP tools and interactive approvals are unavailable. Keep long-running commands in the foreground. Detached/background servers are unsupported and may survive hard runtime loss.",
72
+ "_comment_hooks": "Optional deployment-wide protected Codex hooks. Omit `hooks` for the existing hooks-off behavior. After installing a reviewed hook artifact version, add `hooks` with exactly: artifactRoot, enabled, operatorUid, runtimeId, runtimeRoot, runtimeSha256, serviceUid, and version. Polygram then discovers and pins the exact hook inventory before Codex admission; set enabled:false with the same receipt to roll back cleanly.",
72
73
  "home": "/Users/you/.codex-polygram",
73
74
  "binary": "/Users/you/.local/share/codex/versions/0.145.0/codex",
74
75
  "daemonSecretRoots": [
@@ -0,0 +1,323 @@
1
+ # Codex protected hook artifacts — build, install, upgrade, retire
2
+
3
+ Codex hook trust is content-addressed over the hook's **command string**, and a
4
+ command string names paths, never bytes. A trust stanza therefore certifies
5
+ nothing about what actually executes unless the paths it names are unwritable by
6
+ the account that runs the sessions. This runbook covers the tree that makes them
7
+ unwritable, the one artifact Polygram ships into it, and the operations that
8
+ keep it honest.
9
+
10
+ Everything here is host provisioning plus one deterministic build. It does not
11
+ enable hooks anywhere: hook enablement is a separate, later change, and until it
12
+ lands an installed artifact tree changes nothing about how Codex runs.
13
+
14
+ ## The protected closure
15
+
16
+ Exactly three things are executed or read on a hook fire, and all three are
17
+ attested before every use:
18
+
19
+ 1. **the protected Node runtime** — an operator-owned canonical executable under
20
+ a versioned runtime root,
21
+ 2. **the hook bundle** — one self-contained file, built from checked-in sources,
22
+ 3. **the digest manifest** — rendered from the trusted closure declaration, and
23
+ compared byte for byte against what is installed.
24
+
25
+ The closure is **generated, never authored**: the installer derives it from the
26
+ shipped build output. There is no option, flag, or argument that lets a caller
27
+ name a body, add a member, or drop one.
28
+
29
+ ### How the bundle is confined
30
+
31
+ **The mechanical gate is a syntax policy over the checked-in sources**, not the
32
+ emitted facades. A facade is a convenience for code that already passed the
33
+ gate: any function value hands back the host realm through
34
+ `writeFileSync['constructor']`, and the property name can be assembled at run
35
+ time, so no textual scan and no wrapper can stop a source that wants out. The
36
+ build step therefore parses each source and accepts only:
37
+
38
+ - an enumerated set of syntax nodes — no classes, `this`, tagged templates,
39
+ sequence expressions, `import()`, or module syntax,
40
+ - an enumerated set of free identifiers, resolved **lexically** against the
41
+ scope chain, so a binding introduced in one function or block cannot vouch
42
+ for an ambient name at a use site it never covers; an unresolved identifier
43
+ outside the allowlist is a refusal, so `globalThis`, `global`, `Reflect`,
44
+ `eval`, `Function` and `__dirname` cannot be named at all,
45
+ - non-computed member access only, except a numeric index; `constructor`,
46
+ `prototype`, `__proto__`, `call`, `apply` and `bind` are refused by name, as
47
+ are loader, child-process and external-read members wherever they appear,
48
+ - a **positive member policy on ambient intrinsics** — `Object`, `JSON`,
49
+ `Buffer`, `Date` and friends carry only the members listed for them, so
50
+ `Object.getOwnPropertyDescriptor`, `Object.getPrototypeOf` and
51
+ `Object.getOwnPropertyNames` are refused rather than reasoned about,
52
+ - destructuring only of an import binding or a parameter, never of an
53
+ arbitrary value, and never of a denied property name — lifting `constructor`
54
+ off a function through a pattern is closed the same way member access is,
55
+ - `require` of an exact allowlisted specifier in a plain binding declaration,
56
+ with each imported member checked against that facade's exact surface.
57
+
58
+ The policy governs the **checked-in sources this repository builds**. It is not
59
+ a general sandbox for arbitrary JavaScript, and it makes no claim about code
60
+ that reaches the tree by any other route.
61
+
62
+ The emitted bundle then hands each module a frozen facade as defence in depth:
63
+
64
+ - `require` is a facade loader over a **closed member-level allowlist** —
65
+ `node:crypto.createHash`, `node:fs.writeFileSync`, `node:path.join`. There is
66
+ no read API in the closure at all, and no `child_process` to reach.
67
+ - The raw loader is never bound to a name. It is called once, in the argument
68
+ list of the wrapper, outside the scope any module body runs in.
69
+ - `process` is a curated object (`argv`, `pid`, `setExitCode`, `stdin`), so
70
+ `process.getBuiltinModule`, `process.binding`, `process.env` and
71
+ `process.mainModule` are absent by construction.
72
+ - `globalThis`, `global` and `Function` are bound to `undefined` inside every
73
+ module body.
74
+
75
+ The emitted file is additionally checked so that no specifier outside the
76
+ closure ever reaches the real loader.
77
+
78
+ **The explicitly trusted boundary is the operating system.** The dynamic loader
79
+ and the shared libraries the runtime links against are root-owned on both
80
+ targets and are *not* attested here. An attacker who can replace them already
81
+ owns the host.
82
+
83
+ **What the checks can and cannot see.** Ownership and POSIX mode bits only.
84
+ ACLs, extended attributes, and mount options are not inspected, so a passing
85
+ attestation is a necessary condition for unwritability, not a proof of it.
86
+
87
+ ## Layout
88
+
89
+ ```
90
+ <artifact-root>/ operator-owned, service-unwritable
91
+ artifact-root.json marker proving this tree is ours
92
+ 1.0.0/ one directory per released version
93
+ hook-observer.js the bundle, mode 0644
94
+ manifest.json the generated digest manifest, mode 0644
95
+ quarantine/1.0.0/1/ retired closures, one dir per retirement
96
+ <runtime-root>/
97
+ node-24.4.0/bin/node the protected runtime, mode 0755
98
+ ```
99
+
100
+ Requirements enforced by attestation, each of which fails closed:
101
+
102
+ - every path is **absolute and canonical**; an alias, a relative path, or a
103
+ `PATH`-resolved name is refused,
104
+ - no component of the chain is a **symlink** — there is no `current` alias, and
105
+ a version or runtime identity of `current` / `latest` / `stable` is refused,
106
+ - every canonical **ancestor** up to `/` is owned by root or by the **operator
107
+ uid the caller configured** (never read from the tree), is not
108
+ service-writable, and is traversable by the service account,
109
+ - **closed mode sets**: directories `0755`/`0555`, members `0644`/`0444`,
110
+ runtime `0755`/`0555`. A `0700` runtime is refused — a distinct service
111
+ account could not execute it — and a setuid, setgid, or sticky bit is
112
+ refused anywhere in the chain, on any file, directory, or runtime,
113
+ - every **version-shaped entry** in the root is a real version directory; a
114
+ file or a symlink wearing a version name fails the run closed,
115
+ - each file is a **regular file with `nlink === 1`**, digest-equal to the
116
+ **checked-in declaration** (not to the manifest beside it),
117
+ - the version directory contains **exactly** the declared closure,
118
+ - the manifest **byte-equals a rendering from the trusted expectation**.
119
+
120
+ A digest in a filename is a label, not a permission: a digest-named file in a
121
+ service-writable tree is refused, because the account that can write the tree
122
+ can replace the file between verification and execution.
123
+
124
+ ## Build
125
+
126
+ ```sh
127
+ polygram-build-codex-hook-bundle # regenerate the bundle
128
+ polygram-build-codex-hook-bundle --check # drift gate, exits 1 on drift
129
+ ```
130
+
131
+ From a checkout, the same commands are
132
+ `node scripts/build-codex-hook-bundle.js [--check]`. The bundle at
133
+ `lib/codex/hooks/hook-observer.bundle.js` is checked in and is a pure function
134
+ of `lib/codex/hooks/observer-*.js`. Editing a source without rebuilding is a
135
+ review-visible failure: `--check` exits non-zero, the test suite fails, and the
136
+ installer refuses to install the stale bundle.
137
+
138
+ ## Install a version
139
+
140
+ Run as the operator or root — **never as the service account**, which the
141
+ installer refuses outright. Installs on one artifact root are serialized by an
142
+ exclusive lock, and `--runtime-sha256` is mandatory: the expected runtime digest
143
+ comes from configuration, never from the tree.
144
+
145
+ ```sh
146
+ polygram-codex-hook-artifacts install \
147
+ --artifact-root /opt/polygram/codex-hooks \
148
+ --version 1.0.0 \
149
+ --runtime-root /opt/polygram/runtime \
150
+ --runtime-id node-24.4.0 \
151
+ --runtime-sha256 <expected 64 hex> \
152
+ --operator-uid <operator uid> \
153
+ --service-uid <service uid>
154
+ ```
155
+
156
+ The first install on a root claims the marker, and it does so **only on a
157
+ directory dedicated to hook artifacts** — an otherwise empty one. An existing
158
+ directory with unrelated contents is never claimed, marked, or cleaned.
159
+
160
+ The install stages into a temporary directory inside the artifact root, renames
161
+ it into place, and then attests what landed; it prints the receipt it verified.
162
+ **Record `runtime.sha256` from that receipt into deployment configuration** —
163
+ every later attestation requires it as the expected runtime digest.
164
+
165
+ A staging directory is cleared only after the root marker is verified, and only
166
+ when its name is exactly the one this version would have used; anything else
167
+ with the staging prefix stops the install rather than being reclaimed. Staging
168
+ is created private (`0700`) and each member is written with an exclusive,
169
+ no-follow create; the final modes and the publishing rename happen only after
170
+ every member is in place, so a half-written version is never visible under its
171
+ final name.
172
+
173
+ **Durability is availability here, not integrity.** There is no fsync layer: a
174
+ power loss can leave a staging directory or a partly written file behind. The
175
+ next run fails closed rather than trusting it — attestation refuses anything
176
+ that does not match the declaration — and the leftover staging is recoverable
177
+ by the operator. A new durability subsystem is deliberately out of scope.
178
+
179
+ ### A held lock is never reclaimed automatically
180
+
181
+ A lock file records the holder's pid and a unique token, and release verifies
182
+ that token — a run never removes a lock that is no longer its own. There is no
183
+ liveness-based reclaim: a pid can be reused, and a lock cannot prove which run
184
+ created it, so an existing lock always fails the run closed with
185
+ `CODEX_HOOK_ARTIFACT_LOCKED`.
186
+
187
+ **Recovering a stuck lock is a deliberate operator act.** Confirm no install,
188
+ attestation, or retirement is running against that root — check the recorded pid
189
+ on the host — then, as the account that owns the tree, remove
190
+ `<artifact-root>/install.lock` and re-run the command. Never remove it from the
191
+ service account, and never as part of an automated retry.
192
+
193
+ Verify at any time, from any account:
194
+
195
+ ```sh
196
+ polygram-codex-hook-artifacts attest \
197
+ --artifact-root /opt/polygram/codex-hooks --version 1.0.0 \
198
+ --runtime-root /opt/polygram/runtime --runtime-id node-24.4.0 \
199
+ --runtime-sha256 <expected 64 hex> \
200
+ --operator-uid <operator uid> --service-uid <service uid>
201
+ polygram-codex-hook-artifacts list --artifact-root /opt/polygram/codex-hooks
202
+ ```
203
+
204
+ ## Upgrade
205
+
206
+ A new bundle is a **new version directory**. Versions are never overwritten in
207
+ place — a second install of an existing version is refused — because a session
208
+ that is already running holds the version it started with, and rewriting it
209
+ under that session swaps executed bytes without changing any digest anyone
210
+ checked.
211
+
212
+ The runtime obeys the same contract: a runtime upgrade is a **new runtime
213
+ identity** under the runtime root. One identity may never denote two different
214
+ binaries, and an in-place swap under an existing identity is refused by the next
215
+ install.
216
+
217
+ Because trust is content-addressed over a command string that embeds absolute
218
+ paths, **every path change invalidates every hook hash**: a new artifact version,
219
+ a new runtime identity, or a moved capture root all require reprovisioning each
220
+ hook-enabled Codex home. That is planned work, not self-healing.
221
+
222
+ ## Retire a version
223
+
224
+ ```sh
225
+ polygram-codex-hook-artifacts quarantine \
226
+ --artifact-root /opt/polygram/codex-hooks --version 1.0.0 \
227
+ --runtime-root /opt/polygram/runtime --runtime-id node-24.4.0 \
228
+ --runtime-sha256 <expected 64 hex> \
229
+ --operator-uid <operator uid> --service-uid <service uid> \
230
+ --referenced 1.1.0,1.2.0
231
+ ```
232
+
233
+ Retirement **moves** the version into `<artifact-root>/quarantine/<version>`;
234
+ nothing is deleted. The version must attest first, so only a recognized closure
235
+ in a marked artifact root can be touched at all, and a directory that merely
236
+ looks like a version tree is never moved.
237
+
238
+ The exact root marker is verified **before** a lock file is written into the
239
+ tree — an unmarked directory is never even locked — and the root lock is then
240
+ held across the attestation and the rename, so nothing can be swapped between
241
+ the two.
242
+
243
+ **Precondition, owned by the runtime integration, not by this tooling:** close
244
+ admission for the version first. Retirement verifies what is on disk and moves
245
+ it; it cannot see, and does not coordinate with, a session that activates a
246
+ version between the attestation and the rename. The integration that enables
247
+ hooks must retire the sessions holding a version — and stop new ones from
248
+ starting against it — before retirement runs. This runbook does not add a state
249
+ machine for that.
250
+
251
+ `--referenced` is mandatory and lists every version still referenced by a live
252
+ session or an installed configuration; pass `--referenced ''` only when the
253
+ answer is genuinely none. Retiring a referenced version is refused. Because that
254
+ list cannot be verified from here, quarantine is deliberately recoverable: to
255
+ restore, move the directory back out of `quarantine/` and re-attest. Deleting a
256
+ quarantined version stays a manual operator decision.
257
+
258
+ **A retired version id is spent, exactly.** Retirement moves the closure to
259
+ `quarantine/<version>/<n>`, so the retired identity reads back as the name it
260
+ had — `1.0.0-1` and `1.0.0` are different ids and neither reserves the other.
261
+ Reinstalling a retired name is refused (`CODEX_HOOK_ARTIFACT_VERSION_RESERVED`),
262
+ so a session still holding that version can never be pointed at different bytes
263
+ under the same name. Release the next change under a new id.
264
+
265
+ A quarantine that cannot be read, or that holds anything other than retired
266
+ version directories, fails every install and attestation closed rather than
267
+ quietly freeing an id for reuse. Retired manifests also stay in the
268
+ runtime-identity history, so a retired closure still forbids its runtime id
269
+ denoting different bytes.
270
+
271
+ ## Failure modes
272
+
273
+ | Code | Meaning | First move |
274
+ | --- | --- | --- |
275
+ | `CODEX_HOOK_BUNDLE_UNSAFE_SOURCE` | a source (or the generated bundle) reaches outside the closure | fix the source; the closure is not negotiable |
276
+ | `CODEX_HOOK_BUNDLE_INVALID` | the declared source set, entry, or checked-in bundle is stale | rebuild in the same change as the sources |
277
+ | `CODEX_HOOK_ARTIFACT_UNSAFE` | ownership, mode, symlink, or ancestor chain fails the boundary | fix host provisioning; do not relax the rule |
278
+ | `CODEX_HOOK_ARTIFACT_MISMATCH` | a digest, the manifest, the marker, or the runtime binding disagrees — including a sibling version whose manifest is unreadable | repair the tree; attestation never reads past damage |
279
+ | `CODEX_HOOK_ARTIFACT_MISSING` | version, marker, manifest, or runtime absent | install or provision it |
280
+ | `CODEX_HOOK_ARTIFACT_INVALID` | non-canonical root, alias version or runtime id, missing uid or digest | correct the configuration |
281
+ | `CODEX_HOOK_ARTIFACT_VERSION_EXISTS` | the version is already installed | release a new version |
282
+ | `CODEX_HOOK_ARTIFACT_VERSION_REFERENCED` | a live session still names the version | retire the sessions first |
283
+ | `CODEX_HOOK_ARTIFACT_LOCKED` | a lock file exists, a live staging directory blocks the root, or the lock was replaced mid-run | confirm no run is in flight, then clear the lock by hand |
284
+ | `CODEX_HOOK_ARTIFACT_VERSION_RESERVED` | the version id was retired already | release under a new id |
285
+ | `CODEX_HOOK_COMMAND_INVALID` | descriptor path or argument is unsafe, or an argument is untyped | fix the descriptor; nothing is quoted around a bad token |
286
+ | `CODEX_HOOK_COMMAND_UNATTESTED` | a descriptor path — including one in an argument — is not in the attested closure, or the receipt is not a live attestation | attest the version the command names, and render from that receipt |
287
+
288
+ ### Command arguments are typed
289
+
290
+ A rendered command is built from a descriptor whose arguments are typed, so a
291
+ token's meaning is declared rather than guessed from its spelling:
292
+
293
+ - `{ kind: 'literal', value: 'SessionStart' }` — a word with no path separator
294
+ in it at all,
295
+ - `{ kind: 'attested-path', path: '/…' }` — an absolute canonical path that
296
+ must already be in the attested closure.
297
+
298
+ There is no bare-string form and no relative-path ambiguity: a literal that
299
+ contains a separator is refused, and a path argument outside the closure is
300
+ refused. Rendering also requires the receipt object a full attestation
301
+ returned — identity, not a shape — so a copy, a clone, or a forwarding proxy
302
+ cannot authorise a command.
303
+
304
+ If the boundary cannot be installed on a host — any ancestor writable by the
305
+ service account, any executed input outside the tree, any manifest mismatch —
306
+ hooks cannot be enabled there. Codex itself is unaffected; only hook enablement
307
+ is refused.
308
+
309
+ ## What this does not cover
310
+
311
+ - **Enabling hooks**, rendering `hooks.json`, the trust stanza, provisioning
312
+ state, and rollback: a later change owns those.
313
+ - **A writable capture root.** Every path in a rendered command, arguments
314
+ included, must be in the attested closure, so a hook that writes captures
315
+ needs its capture root brought inside the boundary — or its descriptor
316
+ reshaped — before that command can be rendered.
317
+ - **The memory wrapper and `stop.sh`.** They are not built or installed here.
318
+ Building them through this same build step and proving their generated closure
319
+ is a prerequisite for enabling memory; `stop.sh` shells out by nature, so its
320
+ inputs must come inside the tree or that design must change.
321
+ - **Creating the root-owned tree itself.** Polygram attests and fails closed; it
322
+ cannot create root-owned infrastructure. That is host provisioning on both the
323
+ Mac and the VPS.