polygram 0.38.7 → 0.39.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/.claude-plugin/plugin.json +1 -1
- package/config.example.json +1 -0
- package/docs/2026-08-16-003-codex-protected-hook-artifact-runbook.md +323 -0
- package/lib/attachments.js +5 -2
- package/lib/codex/hook-artifacts.js +1033 -0
- package/lib/codex/hook-bundle.js +296 -0
- package/lib/codex/hook-command.js +192 -0
- package/lib/codex/hook-runtime.js +533 -0
- package/lib/codex/hook-source-policy.js +440 -0
- package/lib/codex/hooks/hook-observer.bundle.js +185 -0
- package/lib/codex/hooks/observer-capture.js +61 -0
- package/lib/codex/hooks/observer-entry.js +74 -0
- package/lib/codex/runtime-controller.js +52 -2
- package/lib/codex/runtime-profile.js +215 -31
- package/lib/handlers/config-ui.js +1 -1
- package/lib/model-costs.js +6 -3
- package/lib/sdk/build-options.js +16 -2
- package/package.json +10 -4
- package/polygram.js +5 -0
- package/scripts/build-codex-hook-bundle.js +74 -0
- package/scripts/codex-hook-artifacts.js +136 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.39.0",
|
|
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/config.example.json
CHANGED
|
@@ -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.
|
package/lib/attachments.js
CHANGED
|
@@ -111,6 +111,7 @@ const MIME_ALLOW = [
|
|
|
111
111
|
/^text\/csv$/,
|
|
112
112
|
// rc.68: archives + markup formats Claude reads natively.
|
|
113
113
|
/^application\/zip$/, /^application\/x-zip-compressed$/,
|
|
114
|
+
/^application\/gzip$/, /^application\/x-gzip$/, /^application\/x-tar$/,
|
|
114
115
|
/^text\/markdown$/,
|
|
115
116
|
/^text\/html$/,
|
|
116
117
|
/^text\/yaml$/, /^application\/yaml$/, /^application\/x-yaml$/,
|
|
@@ -122,8 +123,10 @@ const MIME_ALLOW = [
|
|
|
122
123
|
// the fallback is "trust the filename when MIME is unhelpful" — not "any
|
|
123
124
|
// extension goes." A file named foo.exe with empty MIME stays rejected.
|
|
124
125
|
const EXTENSION_ALLOW = new Set([
|
|
125
|
-
// archives
|
|
126
|
-
'
|
|
126
|
+
// archives — extensionOf() only sees the last dot segment, so
|
|
127
|
+
// "foo.tar.gz" resolves to 'gz' (not 'tar.gz'); listing 'gz'/'tgz'/'tar'
|
|
128
|
+
// separately covers the common compound and short forms.
|
|
129
|
+
'zip', 'gz', 'tgz', 'tar',
|
|
127
130
|
// text / structured data
|
|
128
131
|
'txt', 'md', 'csv', 'json', 'yaml', 'yml', 'xml', 'html',
|
|
129
132
|
// documents
|