@rsrsai/magpie 0.3.3 → 0.3.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/README.md CHANGED
@@ -18,6 +18,8 @@
18
18
 
19
19
  [npm](https://www.npmjs.com/package/@rsrsai/magpie) 已提供公开包,首次发布版本为 0.3.2。[GitHub Releases](https://github.com/risense-ai/respire-magpie/releases) 同时提供分发包与 SHA256SUMS。
20
20
 
21
+ [0.3.3 npm 自动发布验收报告](docs/NPM-PUBLISH-0.3.3.md):Chrome 可信发布连接显示 Valid;GitHub OIDC 发布、公共 npm 安装及签名/attestation 校验通过。
22
+
21
23
  ## 让 AI 安装
22
24
 
23
25
  复制给本地助手:
@@ -60,7 +62,6 @@ magpie plugin add @rsrsai/magpie
60
62
  "limit": 3,
61
63
  "remember": true,
62
64
  "readOnly": false,
63
- "injectRules": true,
64
65
  "showMemoryStatus": false,
65
66
  "capture": false,
66
67
  "archive": true,
@@ -77,11 +78,10 @@ magpie plugin add @rsrsai/magpie
77
78
  | project | 非空时过滤召回、标注新写入并限制后台更新目标;留空可使用当前整个库。不是权限或账号边界。 |
78
79
  | gatewayTimeoutMs | 宿主 Hook 超时,100–10000 毫秒;整个链另有总预算。后台模型处理不占用这段预算。 |
79
80
  | requestTimeoutMs | 前台每个 Runtime 请求含正文读取的预算,100–5000 毫秒。 |
80
- | maxBytes | 记忆与回执注入预算,1024–32768 字节。完整政策另有最多 128 KiB 预算及固定宿主适配说明,每轮完整注入,不裁剪政策;总上下文成本会增加。 |
81
+ | maxBytes | 记忆与回执注入预算,1024–32768 字节。完整政策另有最多 128 KiB 预算及固定宿主适配说明,不裁剪政策。每轮仍读取完整政策;同一会话只在首次和政策变化时注入全文,其余回合只增加记忆与读取回执。 |
81
82
  | limit | 候选 1–5 条,默认 3;超预算或含疑似凭据的正文跳过。 |
82
83
  | remember | 处理“记:”“存:”“记录:”“记住:”“remember:”前缀,默认开启。 |
83
84
  | readOnly | 禁止本插件显式存储、采集及后台写入,不限制其他客户端工具。 |
84
- | injectRules | 兼容旧配置保留;0.3.0 不再允许此项关闭每轮完整政策读取与注入。 |
85
85
  | showMemoryStatus | 提供 Memory、Used 及上次归档状态提示,默认关闭;不能保证客户端显示,严格格式应省略标签。 |
86
86
  | capture | 完成回合原始文字与工具记录保存为 trivial 日记,默认关闭。 |
87
87
  | archive | 每个完成回合保存过程日记,并用独立模型提炼、查重、更新或挂靠与回读,默认开启;必须配置模型。关闭即不满足完整每轮必存。 |
package/docs/FEATURES.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # respire-memory capabilities and boundaries
2
2
 
3
- Applies to **v0.3.2–v0.3.3**. v0.3.3 handles observed Windows/macOS Bun errors during concurrent lock acquisition within the existing deadline, and updates npm distribution documentation. See the [review repair report](REVIEW-RESPONSE-0.3.1.md), [policy-correction report](TEST-REPORT-0.3.0.md) and [historical v0.2.1 acceptance report](TEST-REPORT.md). A working memory round trip is not proof of full engine-policy compliance.
3
+ Applies to **v0.3.4**. Each new turn still reads the complete engine policy; identical policy text is injected once per conversation and again only on change. The ineffective injectRules option is removed. See the [review repair report](REVIEW-RESPONSE-0.3.1.md), [policy-correction report](TEST-REPORT-0.3.0.md) and [historical v0.2.1 acceptance report](TEST-REPORT.md). A working memory round trip is not proof of full engine-policy compliance.
4
4
 
5
5
  ## Agent self-evolution through experience
6
6
 
@@ -15,7 +15,7 @@ This feedback loop changes reusable memory, not model weights. Readback confirms
15
15
  | Capability | Implementation | Evidence / limitation |
16
16
  | --- | --- | --- |
17
17
  | Per-turn read | Recall, complete policy from summary.instructions, selected full records; causal chain for decision/lesson/agreement/error-like hits | Four protocol fixtures; real Runtime complete-policy injection inspected. Same-turn retries reuse durable results |
18
- | Policy presence | Full policy is supplied every new turn, independently of the obsolete injectRules flag | Legacy small recall budgets do not clip policy; separate 128 KiB allowance plus fixed adapter; increases context cost |
18
+ | Policy presence | Complete policy read every new turn; full text supplied on first turn and policy changes, with per-turn read receipts | Identical text is retained once in stable history, including after restart; separate 128 KiB allowance plus fixed adapter. Removed injectRules does not disable mandatory reads |
19
19
  | Explicit remember | Front end reads policy/candidates, reports pending and defers classification and writes | No direct foreground remember that bypasses classification; pending is not saved |
20
20
  | Automatic archival | Default on; detached worker uses a separately configured Chat Completions model | Requires generation setup. Disabled/unconfigured archival is disclosed as incomplete, not successful |
21
21
  | Mandatory process diary | Writable success requires a new trivial write in this archival and readback of its returned ID | No-write nothing-to-store is rejected. Existing long-term facts still must not be duplicated |
@@ -25,7 +25,7 @@ This feedback loop changes reusable memory, not model weights. Readback confirms
25
25
  | Read-only and caller control | Local/magpie-key only; explicit readOnly and engine restrictions remain authoritative | Shared-key fixture rejection checked; old host magpie-key classification remains limited |
26
26
  | Optional raw capture | Trivial raw turn diary when explicitly enabled | Default off; distinct from model-produced process diary; no live raw-capture acceptance claimed |
27
27
  | Settings and status | Settings page, Memory/Used/prior-save hints, adaptive local light/dark icon | v0.2.1 actual Magpie web rendering inspected; hints depend on the answering model |
28
- | Delivery | Three-OS Node/Bun checks and tagged GitHub Release tarball/SHA256 | npm deferred; community registry delivery requires maintainer merge |
28
+ | Delivery | Three-OS Node/Bun checks, tagged GitHub Release and verified npm OIDC publication/install | Public package @rsrsai/magpie; see [0.3.3 publication acceptance](NPM-PUBLISH-0.3.3.md). Community registry delivery still requires maintainer merge |
29
29
 
30
30
  ## Strict requirements that remain unmet
31
31
 
@@ -0,0 +1,34 @@
1
+ # npm publication and OIDC acceptance: 0.3.3
2
+
3
+ Verified on 2026-10-31. Public distribution is **@rsrsai/magpie**, with **latest=0.3.3**. Community PR [553](https://github.com/magpie-community/plugins/pull/553) is a separate distribution/review process and remains unmerged at this checkpoint.
4
+
5
+ ## Authentication and publishing
6
+
7
+ - First publication: the existing v0.3.2 GitHub Release artifact was published after the user completed npm authentication in Chrome. Its registry integrity matched the release artifact; a separate public install and registry-signature audit passed.
8
+ - Trusted Publisher was configured and saved in Chrome: GitHub Actions, **risense-ai/respire-magpie**, **release.yml**, no environment restriction. Direct npm publish is allowed; separate dist-tag management is not. npm also lists its built-in staged-publishing permission. Chrome later showed **Status: Valid**.
9
+ - Repository variable **NPM_PUBLISH_ENABLED=true** enables the existing npm job. It uses a GitHub-hosted runner and id-token: write, after the matching three-OS checks and GitHub Release succeed. No NPM_TOKEN or NODE_AUTH_TOKEN repository secret is configured or referenced for authentication. No long-lived publishing credential was uploaded.
10
+ - Push a tag matching v<package.version> for subsequent releases. Stable versions publish to latest; prereleases publish to next. Do not republish an existing version or move a published tag.
11
+
12
+ ## Reproducible evidence
13
+
14
+ | Check | Result |
15
+ | --- | --- |
16
+ | [Preflight CI at b31c3d4](https://github.com/risense-ai/respire-magpie/actions/runs/41194779076) | Windows/Linux/macOS, Node 24 and Bun 1.3.6: PASS |
17
+ | [v0.3.3 release workflow](https://github.com/risense-ai/respire-magpie/actions/runs/41194886893) | Repeated three-OS verification, GitHub Release and npm OIDC job: PASS |
18
+ | Signed provenance | [Transparency log entry](https://search.sigstore.dev/?logIndex=3613679237), generated by GitHub Actions |
19
+ | [Supplemental community Linux run](https://github.com/risense-ai/respire-magpie/actions/runs/41194907911) | Contribution 10184ce481eb1f15dc90a192fa953b1740d080f2: 4193 pass, 3 skip, 0 fail; 4196 tests/328 files, root checks and package dry-run passed |
20
+ | Registry artifact | Downloaded tarball matches the GitHub Release and registry integrity |
21
+ | Public npm install | Isolated prefix, ignore-scripts: PASS |
22
+ | npm audit signatures | One verified registry signature and one verified attestation: PASS |
23
+ | Installed contents | Runtime modules and icon match the exact v0.3.3 Git tag; embedded icon matches its file |
24
+ | Local Magpie package replacement | Formal v0.3.3 artifact installed; original disabled flag and options preserved |
25
+
26
+ SHA256 of rsrsai-magpie-0.3.3.tgz: **47e3f97353b980662d0127dd4f8a3995caf06375d303f10b9c77b02a2edfc2b1**. The same value is in the [release SHA256SUMS](https://github.com/risense-ai/respire-magpie/releases/tag/v0.3.3).
27
+
28
+ The first v0.3.3 download attempts returned 404 after metadata and provenance were visible. No publishing job was replayed. A registry GET succeeded at 11:07:26 UTC; public installation and the signature/attestation audit then passed. The delay's internal server cause was not established.
29
+
30
+ ## Scope and remaining limits
31
+
32
+ The stronger checks exposed further intermittent Windows/macOS Bun lock-acquisition errors after earlier passing runs. The fixes and failed preflights remain in the [review repair report](REVIEW-RESPONSE-0.3.1.md); the existing contention check verifies 64 increments and four competing child processes. No failed check was skipped to publish.
33
+
34
+ This publication acceptance does not constitute a new live model/native-desktop acceptance. Strict before-send persistence remains unsupported by the current completion callback. Community workflow approval/merge, its dist release and community npm ownership are independent; supplemental upstream CI is not maintainer approval. No private authentication material or local memory evidence is included here.
@@ -1,5 +1,18 @@
1
1
  # Magpie PR 553 review repair report
2
2
 
3
+ ## v0.3.4 follow-up to review 5722498530
4
+
5
+ The reviewer accepted the fresh per-request identity check and display name, and independently reported 4187 passing community tests, 9 skipped, 0 failures at 10184ce. Two remaining blockers concerned repeated policy text and the ineffective injectRules option.
6
+
7
+ - Every newly observed turn still calls recall and reads the complete Runtime policy. A policy hash is persisted with each turn; the full rules/policy block is supplied on the first turn and whenever the policy changes. Unchanged policy remains in its original historical position. Per-turn blocks carry memories and actual read receipts. No mandatory read or automatic process-diary requirement is removed.
8
+ - injectRules is removed from the manifest, validated options, configuration examples and checks. Stale configuration keys are ignored, as other unknown keys are; they cannot disable mandatory policy reads.
9
+ - Failed recall/policy reads report failure through the host without adding an unsaved message to the latest turn. Successfully restored earlier prefixes remain intact.
10
+ - The community registry entry follows its neighbours' multi-line formatting.
11
+
12
+ The existing checks now exercise five consecutive turns, host resets, policy changes, unchanged follow-up and policy reversion across Chat, Responses, Anthropic and Gemini. They verify one copy of unchanged policy, a new copy only on change, one actual recall/prompt per new turn, and byte-identical historical prefixes. Node and the Bun-discovered adapter pass locally on Windows. CI and live npm acceptance will be recorded separately after execution; these local fixtures are not live deployment evidence.
13
+
14
+ The first 0.3.4 preflight [41197398772](https://github.com/risense-ai/respire-magpie/actions/runs/41197398772) passed Linux/macOS but failed the Windows Bun-discovered adapter during the existing 64-increment contention check: mkdir returned EPERM on the exact lock directory. Windows can retain a released directory in delete-pending state. Acquisition now retries that exact syscall/path within the existing deadline, before ownership; sole-owner verification and unknown-write protection are unchanged. The failed candidate is retained as evidence rather than rerun unchanged.
15
+
3
16
  This responds to the review of community head 40fec72. It is an upstream engineering report, not a claim that community CI has approved this package. The community package links upstream reports rather than shipping copies as its own evidence.
4
17
 
5
18
  The first repair candidate, v0.3.1, failed Windows CI with a transient EPERM while removing an empty lock directory under concurrent access. Its tag is retained and no release assets were published. The follow-up candidate is v0.3.2, with bounded Windows cleanup retries. It also corrects a separate empty-recall bug: hiding display hints must not discard the complete engine policy. The existing checks now cover empty recall with hints off across all four adapters.
@@ -23,7 +36,7 @@ The first v0.3.2 main-branch CI run passed macOS/Linux but exposed a separate fi
23
36
  | 11: community README / upstream reports | Community README is rewritten around its actual package, CI entry, monorepo dist/provenance and limitations. Personal paths and upstream release-workflow instructions are removed. Upstream reports are linked and explicitly labeled, not packaged as community CI evidence |
24
37
  | 12: placement / name | Entry/README row moved after the existing memory-hook entries. The requested product/display name respire-memory is retained rather than renamed to Respire |
25
38
 
26
- ## Final automated verification
39
+ ## v0.3.2 verification (historical)
27
40
 
28
41
  - [Three-OS upstream CI at 1a080fb](https://github.com/risense-ai/respire-magpie/actions/runs/41192351640): Windows, Linux and macOS passed with Node 24 and Bun 1.3.6, including the discovered adapter and package dry-run.
29
42
  - [Supplemental community compatibility run](https://github.com/risense-ai/respire-magpie/actions/runs/41192352925) checked out community contribution **df58d566b9ddca3aaced1b490035e0a26468a554** on Linux: **4193 pass, 3 skip, 0 fail**, 4196 tests across 328 files; root checks and the 22-file package dry-run passed. The package's adapter was discovered and passed within the full suite.
@@ -53,4 +66,6 @@ At 7f170e2, Windows passed, while macOS Bun 1.3.6 reported EINVAL on owner-file
53
66
 
54
67
  ### Remaining host limits
55
68
 
56
- The engine's strict before-send persistence requirement still needs an appropriate host callback or a client-owned loop: the current notification is after reply completion. Native desktop rendering, live all-protocol sessions, exhaustive crash/power-loss/account-switch recovery, npm first publication and marketplace approval remain outside the proven result. See the [feature matrix](FEATURES.md) and [0.3.0 correction report](TEST-REPORT-0.3.0.md).
69
+ The [v0.3.3 publication acceptance](NPM-PUBLISH-0.3.3.md) records the final passing three-OS and updated full-community runs, public npm install/signature/attestation verification, and the Chrome-configured OIDC connection showing Valid. The earlier failures above remain part of this report.
70
+
71
+ The engine's strict before-send persistence requirement still needs an appropriate host callback or a client-owned loop: the current notification is after reply completion. Native desktop rendering, live all-protocol sessions, exhaustive crash/power-loss/account-switch recovery and marketplace approval remain outside the proven result. See the [feature matrix](FEATURES.md) and [0.3.0 correction report](TEST-REPORT-0.3.0.md).
package/lock.mjs CHANGED
@@ -52,6 +52,11 @@ export async function acquireLock(path, { waitMs = 5000 } = {}) {
52
52
  if (error.code !== 'ENOENT') throw error;
53
53
  }
54
54
  } catch (error) {
55
+ // Windows can keep a released directory delete-pending before the next mkdir.
56
+ if (process.platform === 'win32' && error.code === 'EPERM' && error.syscall === 'mkdir' && error.path === path) {
57
+ if (Date.now() >= deadline) throw error;
58
+ await delay(25); continue;
59
+ }
55
60
  if (error.code !== 'EEXIST') throw error;
56
61
  try {
57
62
  if ((await lstat(path)).isDirectory()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsrsai/magpie",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "帮助 AI Agent 通过记忆召回、经验积累与后台归档持续自进化",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,7 +46,6 @@
46
46
  "readOnly": false,
47
47
  "archive": true,
48
48
  "showMemoryStatus": false,
49
- "injectRules": true,
50
49
  "generationBaseUrl": "",
51
50
  "generationModel": "",
52
51
  "authorizationEnv": "RESPIRE_MAGPIE_AUTHORIZATION",
package/respire.hooks.mjs CHANGED
@@ -64,14 +64,18 @@ function block(data, budget) {
64
64
  return text;
65
65
  }
66
66
 
67
- async function prepare(o, query, signal, entry, extra = {}) {
67
+ async function prepare(o, query, signal, entry, extra = {}, priorPolicy) {
68
68
  const data = { memories: [], ...extra };
69
69
  const scoped = o.project ? ['--project', o.project] : [];
70
70
  const recalled = SKIP_PREFIX.test(query) ? { status: 'skip', details: [] }
71
71
  : await rpc(o, ['recall', query, '--titles', '--limit', String(o.limit), ...scoped], signal);
72
72
  const policy = await rpc(o, ['prompt'], signal);
73
73
  if (policy.status !== 'ok' || typeof policy.summary?.instructions !== 'string' || !policy.summary.instructions.trim()) throw new Failure('invalid');
74
- data.rules = RULES + '\n\n' + policy.summary.instructions;
74
+ const rules = RULES + '\n\n' + policy.summary.instructions;
75
+ entry.policyHash = hash(rules);
76
+ // Read every turn; retain the full text once in history and again on change.
77
+ if (entry.policyHash !== priorPolicy) data.rules = rules;
78
+ data.reads = { recall: recalled.status === 'skip' ? 'skipped' : 'complete', policy: 'complete', policyHash: entry.policyHash };
75
79
  data.workflow = { archive: o.archive, generationConfigured: !!(o.generationBaseUrl && o.generationModel),
76
80
  note: o.readOnly ? '只读,禁止写入。' : !o.archive ? '归档已关闭,未满足每轮读存流程。'
77
81
  : !o.generationBaseUrl || !o.generationModel ? '尚未配置归档模型,不能完成每轮存储。'
@@ -147,13 +151,13 @@ export async function onRequest(body, ctx) {
147
151
  ctx.state.respire = { body: structuredClone(body), o, key: digest(account, keyOf(latest)), account, sessionKey,
148
152
  storeRequest: STORE_PREFIX.test(originalQuery) };
149
153
  }
150
- let modified = false;
154
+ let modified = false, priorPolicy;
151
155
  const priorTurn = async t => {
152
156
  let prior = cache.get(keyOf(t));
153
157
  if (!prior) {
154
158
  const saved = await store.get('turn-' + digest(account, keyOf(t)));
155
159
  if (saved?.phase === 'done' && saved.text) {
156
- prior = { text: saved.text, storage: saved.storage, bytes: bytes(saved.text) }; retain(cache, keyOf(t), prior);
160
+ prior = { text: saved.text, storage: saved.storage, policyHash: saved.policyHash, bytes: bytes(saved.text) }; retain(cache, keyOf(t), prior);
157
161
  }
158
162
  }
159
163
  return prior;
@@ -161,6 +165,7 @@ export async function onRequest(body, ctx) {
161
165
  for (const t of users) {
162
166
  if (t === latest) continue;
163
167
  const prior = await priorTurn(t);
168
+ if (prior?.policyHash) priorPolicy = prior.policyHash;
164
169
  if (prior?.text && !t.text.endsWith(prior.text)) { t.append(prior.text); modified = true; }
165
170
  }
166
171
  if (all.at(-1) !== latest) {
@@ -178,14 +183,14 @@ export async function onRequest(body, ctx) {
178
183
  // Share the in-flight operation across concurrent requests of the same turn.
179
184
  retain(cache, key, entry);
180
185
  entry.pending = store.change(durableKey, async (saved, persist) => {
181
- if (saved?.phase === 'done') { entry.storage = saved.storage; return saved.text; }
186
+ if (saved?.phase === 'done') { entry.storage = saved.storage; entry.policyHash = saved.policyHash; return saved.text; }
182
187
  if (saved?.phase === 'writing') return block({ storage: { status: 'uncertain', message: '该回合曾开始写入,结果未确认。先查库核实,禁止重试写入。' } }, o.maxBytes);
183
188
  await persist({ phase: 'processing' });
184
189
  entry.beforeWrite = () => persist({ phase: 'writing' });
185
190
  const extra = {};
186
191
  if (o.showMemoryStatus && session.lastArchive && session.lastArchive.source !== session.reportedSource) extra.priorSave = session.lastArchive;
187
- const text = await prepare(o, STORE_PREFIX.test(originalQuery) ? originalQuery : q, ctx.signal, entry, extra);
188
- await persist({ phase: 'done', text, storage: entry.storage });
192
+ const text = await prepare(o, STORE_PREFIX.test(originalQuery) ? originalQuery : q, ctx.signal, entry, extra, priorPolicy);
193
+ await persist({ phase: 'done', text, storage: entry.storage, policyHash: entry.policyHash });
189
194
  if (extra.priorSave) {
190
195
  try { await store.change('session-' + sessionKey, async (current = {}, save) => save({ ...current, reportedSource: extra.priorSave.source })); }
191
196
  catch { ctx.report?.('session-notification-failed'); }
@@ -205,8 +210,7 @@ export async function onRequest(body, ctx) {
205
210
  return modified ? changed : undefined;
206
211
  } catch (error) {
207
212
  ctx.report?.(error instanceof Failure ? error.code : 'unavailable');
208
- latest.append(block({ workflow: { status: 'unavailable', note: '本轮召回或完整政策读取失败,未满足答前读忆流程;不得宣称记忆流程已完成。' } }, o.maxBytes));
209
- return changed;
213
+ return modified ? changed : undefined;
210
214
  }
211
215
  }
212
216
 
package/runtime.mjs CHANGED
@@ -26,14 +26,14 @@ export function options(raw = {}) {
26
26
  const o = { url: url.href, project: raw.project ?? '', timeout: raw.requestTimeoutMs ?? 1500,
27
27
  maxBytes: raw.maxBytes ?? 8000, limit: raw.limit ?? 3, remember: raw.remember ?? true, capture: raw.capture ?? false,
28
28
  readOnly: raw.readOnly ?? false, archive: raw.archive ?? true, showMemoryStatus: raw.showMemoryStatus ?? false,
29
- injectRules: raw.injectRules ?? true, stateDir: raw.stateDir ?? '', generationBaseUrl: raw.generationBaseUrl ?? '',
29
+ stateDir: raw.stateDir ?? '', generationBaseUrl: raw.generationBaseUrl ?? '',
30
30
  generationModel: raw.generationModel ?? '', authorizationEnv: raw.authorizationEnv ?? 'RESPIRE_MAGPIE_AUTHORIZATION' };
31
31
  if (typeof o.project !== 'string' || o.project.length > 256 || /[\x00-\x1f\x7f]/.test(o.project) ||
32
32
  typeof o.remember !== 'boolean' || typeof o.capture !== 'boolean' ||
33
33
  !Number.isInteger(o.timeout) || o.timeout < 100 || o.timeout > 5000 ||
34
34
  !Number.isInteger(o.maxBytes) || o.maxBytes < 1024 || o.maxBytes > 32768 ||
35
35
  !Number.isInteger(o.limit) || o.limit < 1 || o.limit > 5) throw new Failure('invalid');
36
- for (const key of ['readOnly', 'archive', 'showMemoryStatus', 'injectRules']) if (typeof o[key] !== 'boolean') throw new Failure('invalid');
36
+ for (const key of ['readOnly', 'archive', 'showMemoryStatus']) if (typeof o[key] !== 'boolean') throw new Failure('invalid');
37
37
  for (const key of ['stateDir', 'generationBaseUrl', 'generationModel', 'authorizationEnv']) if (typeof o[key] !== 'string' || o[key].length > 2048 || /[\x00-\x1f\x7f]/.test(o[key])) throw new Failure('invalid');
38
38
  if (o.stateDir && !isAbsolute(o.stateDir)) throw new Failure('invalid');
39
39
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(o.authorizationEnv)) throw new Failure('invalid');