@sigrun/sigrun 0.2.1 → 0.3.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/CHANGELOG.md +50 -0
- package/README.md +234 -1
- package/dist/cli.js +117 -19
- package/dist/config.js +21 -2
- package/dist/extension.d.ts +7 -1
- package/dist/harness/ambient.js +2 -0
- package/dist/harness/blocks/state.js +1 -0
- package/dist/harness/boot.js +86 -6
- package/dist/harness/context.js +25 -0
- package/dist/harness/event-bus.js +25 -14
- package/dist/harness/identity/accounts.js +3 -2
- package/dist/harness/identity/index.js +2 -1
- package/dist/harness/identity/invites.js +26 -9
- package/dist/harness/identity/link-sweep.js +47 -0
- package/dist/harness/identity/typed-username.js +15 -0
- package/dist/harness/identity/users.js +10 -7
- package/dist/harness/interactions.js +2 -0
- package/dist/harness/loader.js +57 -1
- package/dist/harness/migration-snapshot.js +97 -0
- package/dist/harness/reports.js +100 -0
- package/dist/harness/seam-host.js +11 -1
- package/dist/harness/settings.js +52 -4
- package/dist/jobs/docs.js +3 -0
- package/dist/jobs/fire.js +2 -0
- package/dist/jobs/parked.js +38 -0
- package/dist/jobs/task.js +37 -8
- package/dist/serve.js +23 -14
- package/dist/server/auth/ip.js +9 -4
- package/dist/server/auth/own-host.js +115 -0
- package/dist/server/auth/pipeline.js +64 -48
- package/dist/server/auth/proxy-diagnostic.js +60 -0
- package/dist/server/auth/proxy-identity.js +134 -0
- package/dist/server/auth/proxy-only.js +28 -0
- package/dist/server/login/resolve.js +88 -26
- package/dist/server/routes/admin.js +97 -0
- package/dist/server/routes/login.js +8 -0
- package/dist/server/routes/metrics.js +18 -2
- package/dist/server/routes/onboarding.js +97 -37
- package/dist/server/session-meta.js +1 -0
- package/dist/server/shutdown.js +93 -16
- package/dist/settings/registry.js +6 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +7 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/usernames.d.ts +8 -0
- package/node_modules/@sigrun/shared/dist/usernames.js +39 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +6 -4
- package/node_modules/@sigrun/shared/package.json +5 -1
- package/package.json +14 -12
- package/webui/dist/assets/{index-SO2W4jjk.js → index-CEOzj1C4.js} +11 -11
- package/webui/dist/index.html +1 -1
- package/dist/server/login/usernames.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Parked-task visibility and recovery: pi-durable reports now reach logs,
|
|
14
|
+
metrics, alerts, and job status, and administrators can list and retry
|
|
15
|
+
parked tasks through the admin API.
|
|
16
|
+
- Proxy identity links with case-insensitive matching, plus typed-username
|
|
17
|
+
normalization for `user add --proxy`, `link`, and `unlink`.
|
|
18
|
+
- An optional chart `networkPolicy`, `OnboardingState.proxyIdentity.subject`,
|
|
19
|
+
the ProxyUnidentified screen, and the shared `./usernames` export.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Upgraded the Earendil runtime to pi-durable 1.4.0 (with chord and pi-ai
|
|
23
|
+
1.4.0), including durable schedule sleeps, retry backoffs that survive
|
|
24
|
+
restarts, and the crash-loop parked-task guard.
|
|
25
|
+
- Extension shutdown now runs in parallel within a fixed budget, with the
|
|
26
|
+
chart's termination grace period matched to that budget.
|
|
27
|
+
- The loader quarantines extensions whose declared Earendil peer range
|
|
28
|
+
excludes the host version instead of allowing an incompatible extension
|
|
29
|
+
to fail later at runtime.
|
|
30
|
+
- `settings.compaction.modelOverrides` supports per-model compaction
|
|
31
|
+
settings while preserving environment-over-file precedence.
|
|
32
|
+
- Proxy-only non-loopback boots refuse unvouched account creation, including
|
|
33
|
+
login and code paths, and mint no setup code there. Login derives from the
|
|
34
|
+
full email; boot sweeps orphaned proxy links, gates own-host peers, and
|
|
35
|
+
throttles proxy diagnostics.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- CLI empty-string identity values now mean "unset".
|
|
39
|
+
|
|
40
|
+
### Breaking
|
|
41
|
+
- When `SIGRUN_API_TOKEN` is set, `SIGRUN_API_TOKEN_USER` must be a valid
|
|
42
|
+
username key (the validation error includes a derivation suggestion). The
|
|
43
|
+
user value is validated only when the token is set.
|
|
44
|
+
|
|
45
|
+
### Upgrade notes
|
|
46
|
+
- **One-way storage migration:** the first 0.3.0 boot migrates the SQLite
|
|
47
|
+
durable schema to pi-durable 1.4.0's format. Before opening storage, boot
|
|
48
|
+
writes a crash-consistent snapshot beside the database named
|
|
49
|
+
`<storagePath>.pre-durable-schema-v<CURRENT>-<UTC stamp>.bak`. Snapshots
|
|
50
|
+
are never deleted automatically. If a needed snapshot cannot be written,
|
|
51
|
+
boot stops before migration. `SIGRUN_SKIP_MIGRATION_SNAPSHOT=1` is the
|
|
52
|
+
emergency opt-out for an operator with an independent backup.
|
|
53
|
+
- **Rollback to 0.2.x (SQLite):** stop sigrun, restore the `.bak` over the
|
|
54
|
+
database file, remove the database's `-wal` and `-shm` sidecars, then
|
|
55
|
+
deploy 0.2.x. Anything committed after the upgrade is lost.
|
|
56
|
+
- **JSONL storage:** there is no snapshot because its schema does not
|
|
57
|
+
change. A 0.2.x process nevertheless never resumes a task left `sleeping`
|
|
58
|
+
by 0.3.0, so rollback can strand that task.
|
|
59
|
+
|
|
10
60
|
## [0.2.1] - 2026-10-28
|
|
11
61
|
|
|
12
62
|
### Added
|
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ still override it per field.
|
|
|
175
175
|
| `SIGRUN_PORT` | positive int | `3000` | `server.port` | HTTP listen port. |
|
|
176
176
|
| `SIGRUN_LISTEN_HOST` | host | `::` | — | HTTP listen host — the environment equivalent of the programmatic `serve({host})` option (the option wins when both are set). One resolved value drives the bind AND the auth defaults: a loopback bind derives the no-auth workstation posture, any other bind derives `token` auth. `GET /v1/meta` reports the effective listener (`listener.host`/`port`/`families`). |
|
|
177
177
|
| `SIGRUN_STORAGE_PATH` | path | `<dataDir>/sigrun.db` | `server.storagePath` | SQLite database location. Relative values resolve against the data root (absolute values verbatim; the cwd-relative `data/sigrun.db` default is retired). |
|
|
178
|
+
| `SIGRUN_SKIP_MIGRATION_SNAPSHOT` | `1` | *(unset)* | — | Skip the automatic pre-migration SQLite snapshot when set to `1`. Emergency escape hatch for an operator who already has an independent backup; without it, a needed snapshot failure stops boot before the one-way durable schema migration. |
|
|
178
179
|
| `SIGRUN_WORKSPACE_PATH` | path | `<dataDir>/workspace` | `server.workspacePath` | Root of the static local workspace every conversation's tools run against. The default rides the data root (chart/compose deploys set the path explicitly); it is never persisted on first boot. |
|
|
179
180
|
| `SIGRUN_DATA_DIR` | path | `$XDG_DATA_HOME/sigrun` or `~/.local/share/sigrun` | `server.dataDir` | Data root (the DB default and extension data dirs). Env/file values win verbatim. |
|
|
180
181
|
| `SIGRUN_CACHE_DIR` | path | `$XDG_CACHE_HOME/sigrun` or `~/.cache/sigrun` | `server.cacheDir` | Cache root (regenerable; never ensured at boot). Env/file values win verbatim. |
|
|
@@ -183,6 +184,7 @@ still override it per field.
|
|
|
183
184
|
| — | — | *(none)* | `gitDir` is always `<configDir>/git` | Git extension checkouts clone here (pi layout). Not env-configurable. |
|
|
184
185
|
| `SIGRUN_WEBUI_BUNDLE` | path | `webui/dist` | `server.webuiBundle` | Web UI bundle directory; explicit-but-missing = boot error, default-missing = API-only. |
|
|
185
186
|
| `SIGRUN_CONFIG_DIR` | path | `~/.config/sigrun` | *env-only* | The settings file's own directory. |
|
|
187
|
+
| `SIGRUN_API_TOKEN_USER` | username key | `sigrun` | *env-only* | User for `SIGRUN_API_TOKEN`. Must already be a valid username key (`^[a-z0-9][a-z0-9._-]*$`, at most 128 characters); invalid values fail boot with a derivation suggestion. Validated only when `SIGRUN_API_TOKEN` is non-empty. |
|
|
186
188
|
| `SIGRUN_COMPACTION_ENABLED` | `true`/`false` | `true` | `server.compaction.enabled` | Context compaction on/off. |
|
|
187
189
|
| `SIGRUN_COMPACTION_RESERVE_TOKENS` | positive int | `16384` | `server.compaction.reserveTokens` | Headroom reserved for the model's reply. |
|
|
188
190
|
| `SIGRUN_COMPACTION_KEEP_RECENT_TOKENS` | positive int | `20000` | `server.compaction.keepRecentTokens` | Recent context retained verbatim through a compaction. |
|
|
@@ -192,6 +194,32 @@ still override it per field.
|
|
|
192
194
|
| `SIGRUN_BACKUP_S3_PREFIX` | string | *(unset)* | `server.backup.s3.prefix` | Key prefix within the bucket. |
|
|
193
195
|
| `SIGRUN_BACKUP_S3_ENDPOINT` | URL | AWS default | `server.backup.s3.endpoint` | Custom S3 endpoint (S3-compatible stores). |
|
|
194
196
|
|
|
197
|
+
Per-model overrides: `server.compaction.modelOverrides` is a map keyed by
|
|
198
|
+
the exact `provider/modelId` string pi-durable composes — note that a
|
|
199
|
+
chain-shaped model id doubles the prefix. Use it when a small-window model
|
|
200
|
+
must sit alongside the large-window default policy (lower
|
|
201
|
+
`reserveTokens`/`keepRecentTokens`/`backgroundTokens`, or `enabled: false`
|
|
202
|
+
for a model that must never compact):
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
"server": {
|
|
206
|
+
"compaction": {
|
|
207
|
+
"reserveTokens": 16384,
|
|
208
|
+
"modelOverrides": {
|
|
209
|
+
// Synthetic example: provider "bifrost", model id "bifrost/small-chat".
|
|
210
|
+
"bifrost/bifrost/small-chat": { "reserveTokens": 4096, "keepRecentTokens": 6000 }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Each entry is a partial policy (the same fields and minimums as the parent
|
|
217
|
+
block); unset fields inherit the global policy. Boot-time lifetime like the
|
|
218
|
+
rest of compaction, and there is no env spelling. After the boot catalog
|
|
219
|
+
refresh an override key that matches no known model logs one warning
|
|
220
|
+
(`compaction override "<key>" matches no known model`) — never an error,
|
|
221
|
+
since catalogs move.
|
|
222
|
+
|
|
195
223
|
### settings.json schema
|
|
196
224
|
|
|
197
225
|
```jsonc
|
|
@@ -218,7 +246,10 @@ still override it per field.
|
|
|
218
246
|
"enabled": true,
|
|
219
247
|
"reserveTokens": 16384, // positive integer
|
|
220
248
|
"keepRecentTokens": 20000, // positive integer
|
|
221
|
-
"backgroundTokens": 32768
|
|
249
|
+
"backgroundTokens": 32768, // non-negative integer (0 = off)
|
|
250
|
+
"modelOverrides": { // per-model partial policies, keyed by exact provider/modelId
|
|
251
|
+
"bifrost/bifrost/small-chat": { "reserveTokens": 4096 }
|
|
252
|
+
}
|
|
222
253
|
},
|
|
223
254
|
"backup": { // target: { "path": "/backups" } XOR { "s3": { "bucket": "...", "prefix": "...", "endpoint": "..." } }
|
|
224
255
|
"staleAfterHours": 36 // positive number: health.backup degrades past this marker age
|
|
@@ -307,6 +338,64 @@ configurable) when the request comes from a peer inside
|
|
|
307
338
|
proxy and nothing else: every peer in `trustedProxies` can forge any
|
|
308
339
|
identity, including an admin.
|
|
309
340
|
|
|
341
|
+
**Which address to list.** `trustedProxies` must list the address the
|
|
342
|
+
proxy **connects from**, as the pod sees it — not the proxy's own
|
|
343
|
+
Service or DNS name. The same list also gates trust in
|
|
344
|
+
`X-Forwarded-Host` (the Origin check) and `X-Forwarded-Proto` (the
|
|
345
|
+
Secure-cookie decision): a peer outside the list cannot steer either.
|
|
346
|
+
On a dual-stack cluster the Service may be reached over either family
|
|
347
|
+
(the Service can be IPv6-first while the proxy still connects over
|
|
348
|
+
IPv4), so list every address the proxy actually sources from.
|
|
349
|
+
|
|
350
|
+
**Cilium Gateway API / Ingress.** The connect-from address is the
|
|
351
|
+
per-node **Cilium ingress IP**, allocated from that node's pod CIDR —
|
|
352
|
+
enumerate them with:
|
|
353
|
+
|
|
354
|
+
```sh
|
|
355
|
+
kubectl get ciliumnodes -o jsonpath='{..spec.ingress}'
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Listing those addresses exactly is the tightest posture. Trusting the
|
|
359
|
+
whole pod CIDR (e.g. `10.42.0.0/16`, `fd00:42::/64`) is acceptable
|
|
360
|
+
**only together with a policy admitting the `ingress` entity** — the
|
|
361
|
+
chart's `networkPolicy.enabled` option installs that
|
|
362
|
+
CiliumNetworkPolicy, whose default also admits `host` for kubelet
|
|
363
|
+
probes. Without it, any pod in the CIDR can forge
|
|
364
|
+
identity headers directly to the Service. Note that the `ingress`
|
|
365
|
+
entity covers **every Cilium gateway in the cluster**, so every route
|
|
366
|
+
to sigrun must pass through forward auth; a gateway route that skips
|
|
367
|
+
it is an open impersonation path.
|
|
368
|
+
|
|
369
|
+
**Forward-auth header hygiene.** Forward auth must return **every
|
|
370
|
+
identity header sigrun reads** on every authenticated response, and
|
|
371
|
+
its upstream-header mode must **override, not append** — Envoy's
|
|
372
|
+
`allowed_upstream_headers`, not `allowed_upstream_headers_to_append`.
|
|
373
|
+
With append semantics a client can supply the headers forward auth
|
|
374
|
+
omits and sigrun will read the client's copies.
|
|
375
|
+
|
|
376
|
+
**Own-host peers (C5).** A trusted **range** never vouches for
|
|
377
|
+
sigrun's own addresses — loopback or any local interface address —
|
|
378
|
+
without the shared secret, because every local process shares those
|
|
379
|
+
source addresses. An **exactly listed** local address (a bare address,
|
|
380
|
+
or a `/32` / `/128` entry) does vouch, as an explicit operator
|
|
381
|
+
decision, and boot logs a warning for each such entry when no shared
|
|
382
|
+
secret is configured.
|
|
383
|
+
|
|
384
|
+
**Rejection diagnostics.** Each rejection logs one sanitized line
|
|
385
|
+
(subject to the throttle below) naming the socket peer, the method, a
|
|
386
|
+
short reason, and the route, when known — never tokens, cookies, or
|
|
387
|
+
identity-header values. The throttle emits one line per reason per 60 s,
|
|
388
|
+
with `(N suppressed)` on the next line for that reason. The reasons:
|
|
389
|
+
|
|
390
|
+
| Reason | Meaning |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `untrusted proxy` | The socket peer is outside `trustedProxies`. |
|
|
393
|
+
| `own-host peer without shared secret` | The peer is a local address trusted only through a range, and no shared secret is configured — see C5 above. |
|
|
394
|
+
| `missing or wrong shared secret (<header> header)` | The named secret header was absent or did not match `SIGRUN_PROXY_SECRET`. |
|
|
395
|
+
| `missing identity header (<header>)` | The proxy vouched but sent no user header — forward auth is not returning every identity header. |
|
|
396
|
+
| `unusable identity claim` | The vouched username cannot be a directory user (403 `invalid_username`). |
|
|
397
|
+
| `untrusted identity source` | The user does not exist and `autoProvision` is off (403 `user_not_provisioned`). |
|
|
398
|
+
|
|
310
399
|
**Stored groups and group-derived admin.** `server.auth.adminGroups`
|
|
311
400
|
lists groups (Authelia's `Remote-Groups`, for example) whose members act as
|
|
312
401
|
admins. Core stores groups per authenticating source: `proxy` for validated
|
|
@@ -329,6 +418,106 @@ until then. Token and session requests only read the stored record; they
|
|
|
329
418
|
never accept groups from the request. People-scoped conversation shares may
|
|
330
419
|
also name groups, matched against the same stored groups.
|
|
331
420
|
|
|
421
|
+
### Usernames
|
|
422
|
+
|
|
423
|
+
Every external identity — proxy header, OIDC subject, invite claim,
|
|
424
|
+
CLI argument — resolves to one **directory user key**. The key is the
|
|
425
|
+
stable identifier: it names the user's storage directory
|
|
426
|
+
(`<dataDir>/users/<user>`), their config directories, their per-user
|
|
427
|
+
extension stores, and it appears in tool registrations as
|
|
428
|
+
`<slug>@<user>`, split at the **first `@`**. The key pattern is
|
|
429
|
+
therefore a safety rule, not a style choice:
|
|
430
|
+
|
|
431
|
+
- lowercase letters, digits, `.`, `_`, `-` only, and no `/` against
|
|
432
|
+
traversal (case-insensitive filesystems would otherwise let `Alice`
|
|
433
|
+
and `alice` collide on one directory);
|
|
434
|
+
- must start with a letter or digit (no leading `.` against hidden
|
|
435
|
+
paths or `..` keys);
|
|
436
|
+
- no `@` — it is the tool-registration delimiter;
|
|
437
|
+
- at most 128 characters, bounding key length for storage and tool
|
|
438
|
+
names.
|
|
439
|
+
|
|
440
|
+
`deriveUsername(input)` maps any input onto that pattern: Unicode NFKD
|
|
441
|
+
and strip combining marks, lowercase, `@` → `_`, every other disallowed
|
|
442
|
+
character → `-` (no collapsing), strip leading non-alphanumerics,
|
|
443
|
+
truncate to 128. Empty result → no usable key: proxy authentication
|
|
444
|
+
refuses with 403 `invalid_username`, typed forms with 400, and login
|
|
445
|
+
derivation falls back to the literal key `user`. The normative vectors:
|
|
446
|
+
|
|
447
|
+
| Input | Key |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `mikemacleod` | `mikemacleod` |
|
|
450
|
+
| `Mike` | `mike` |
|
|
451
|
+
| `mike@example.ca` | `mike_example.ca` |
|
|
452
|
+
| `mike@example.com` | `mike_example.com` |
|
|
453
|
+
| `Mike.Smith@Example.COM` | `mike.smith_example.com` |
|
|
454
|
+
| `mike+test@example.com` | `mike-test_example.com` |
|
|
455
|
+
| `José García` | `jose-garcia` |
|
|
456
|
+
| `DOMAIN\mike` | `domain-mike` |
|
|
457
|
+
| `Alice Smith` | `alice-smith` |
|
|
458
|
+
| `.hidden` | `hidden` |
|
|
459
|
+
| `mike.` | `mike.` (valid key, unchanged) |
|
|
460
|
+
| `` (empty) | `undefined` (login: `user`) |
|
|
461
|
+
| `@@@` | `undefined` (login: `user`) |
|
|
462
|
+
|
|
463
|
+
Derivation is idempotent and every valid key is a fixed point. On the
|
|
464
|
+
proxy path, a folded subject that is itself a valid key adopts the
|
|
465
|
+
existing user with that key; a derived-key collision under login
|
|
466
|
+
auto-provisioning takes the next free suffix instead.
|
|
467
|
+
|
|
468
|
+
**Proxy uniqueness assumption (deployment requirement).** Proxy
|
|
469
|
+
identity matching is case-insensitive: the proxy's usernames are
|
|
470
|
+
assumed unique case-insensitively, so `Alice` and `alice` are the same
|
|
471
|
+
person. Authelia/LDAP satisfy this. On a proxy where they are distinct
|
|
472
|
+
accounts, `Alice` would resolve to `alice`'s account — including admin
|
|
473
|
+
rights. Verify your identity source satisfies this before enabling the
|
|
474
|
+
proxy method.
|
|
475
|
+
|
|
476
|
+
**Derived vs typed collisions.** When a derived key is already taken
|
|
477
|
+
(proxy auto-provisioning, login auto-provisioning), the next free
|
|
478
|
+
suffix is chosen automatically: `alice`, then `alice-2`, `alice-3`,
|
|
479
|
+
…. Typed names — `sigrun user add`, the invite claim form, the
|
|
480
|
+
onboarding claim form — **refuse** with `username_taken` instead: an
|
|
481
|
+
admin choosing a name never gets silently suffixed.
|
|
482
|
+
|
|
483
|
+
**Login rule.** A new login user with no username claim derives from
|
|
484
|
+
the **full email** (`mike@example.com` → `mike_example.com`), never
|
|
485
|
+
just the local part. Existing linked users are unchanged — links are
|
|
486
|
+
matched by exact subject, not by re-deriving the key.
|
|
487
|
+
|
|
488
|
+
**Recipes.** Pre-provision a proxy user (and optionally grant admin):
|
|
489
|
+
|
|
490
|
+
```sh
|
|
491
|
+
sigrun user add alice --proxy Alice --admin
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Link an additional proxy identity to an existing user:
|
|
495
|
+
|
|
496
|
+
```sh
|
|
497
|
+
sigrun user link alice --proxy alice.smith
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Crash-orphan repair.** If the process crashed between writing the
|
|
501
|
+
user row and writing the proxy link, the user exists but cannot
|
|
502
|
+
resolve. The boot sweep logs one line per orphan:
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
identity: proxy user "alice-2" has no proxy link; repair with: sigrun user link alice-2 --proxy <username>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Choose the repair that matches the intended owner:
|
|
509
|
+
|
|
510
|
+
- If the orphaned `-2` user should own the identity, run the printed
|
|
511
|
+
`sigrun user link <key> --proxy <username>` command.
|
|
512
|
+
- If the correct account already exists, delete the duplicate as an
|
|
513
|
+
admin with `DELETE /v1/users/alice-2?transferTo=alice`, where
|
|
514
|
+
`transferTo` names that existing account and receives its transferable
|
|
515
|
+
resources.
|
|
516
|
+
|
|
517
|
+
These are alternatives, not a sequence: deleting a user also removes
|
|
518
|
+
its links, so linking and then deleting the same `-2` user would undo
|
|
519
|
+
the repair.
|
|
520
|
+
|
|
332
521
|
### External sign-in (OIDC and OAuth 2)
|
|
333
522
|
|
|
334
523
|
The web sign-in screen lists configured external providers above the token
|
|
@@ -480,6 +669,20 @@ admin) or by trusted-proxy auto-provision; admins can transfer or remove
|
|
|
480
669
|
users (`PATCH`/`DELETE /v1/users/{user}` — deletion transfers their
|
|
481
670
|
conversations).
|
|
482
671
|
|
|
672
|
+
**Proxy-only onboarding.** On a proxy-only server bound beyond
|
|
673
|
+
loopback, onboarding happens entirely **through the proxy**: the first
|
|
674
|
+
claim and every later sign-in arrive as trusted proxy identities, no
|
|
675
|
+
setup or invite codes apply (no setup code is minted, and an invite
|
|
676
|
+
code cannot be redeemed without a vouch), and an unvouched
|
|
677
|
+
account-creating request is refused with 403
|
|
678
|
+
`proxy_identity_required`. A browser reaching sigrun without a trusted
|
|
679
|
+
proxy identity sees a ProxyUnidentified screen instead of a token or
|
|
680
|
+
setup-code form. As a deliberate, bounded exception,
|
|
681
|
+
`/v1/auth/callback/:provider` still lets a login provider with
|
|
682
|
+
`policy.autoProvision` create its own non-admin account on a proxy-only
|
|
683
|
+
server, although the resulting session cannot authenticate because the
|
|
684
|
+
token method is off.
|
|
685
|
+
|
|
483
686
|
**Sharing.** Conversations are private to their owner by default. The
|
|
484
687
|
owner can share one — household-wide or with named users and groups,
|
|
485
688
|
`read` or `write` — through the web UI's ⋯ menu; `write`
|
|
@@ -1075,6 +1278,34 @@ launcher) is not supervised either; there the callout says "Restart
|
|
|
1075
1278
|
sigrun to apply" with no button. `GET /v1/meta` reports the posture
|
|
1076
1279
|
(`supervised`).
|
|
1077
1280
|
|
|
1281
|
+
## Parked tasks
|
|
1282
|
+
|
|
1283
|
+
A task cut off repeatedly without progress (a crash loop) is parked by
|
|
1284
|
+
pi-durable's restart limit: it shows `blocked` with reason
|
|
1285
|
+
`restart_limit` and does not run again until an administrator retries
|
|
1286
|
+
it. The `SigrunTaskParked` alert fires on this. Two admin-only
|
|
1287
|
+
endpoints inspect and recover parked tasks:
|
|
1288
|
+
|
|
1289
|
+
- `GET /v1/admin/tasks?state=blocked` — lists blocked tasks with
|
|
1290
|
+
`{id, kind, reason, conversationId, owner?, extension?}` per row
|
|
1291
|
+
(owner is the conversation's session owner when it has one; extension
|
|
1292
|
+
is the registered task definition's owner when known).
|
|
1293
|
+
- `POST /v1/admin/tasks/:id/retry` — re-runs a restart-parked task:
|
|
1294
|
+
204 on success, 404 `unknown_task` for an unknown id, 409
|
|
1295
|
+
`not_parked` for a task that isn't parked (including one blocked for
|
|
1296
|
+
another reason, such as `missing_task`). A retried schedule job's
|
|
1297
|
+
status returns to `waiting` as the task runs again.
|
|
1298
|
+
|
|
1299
|
+
```sh
|
|
1300
|
+
# List blocked tasks (admin token).
|
|
1301
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
1302
|
+
"http://localhost:3000/v1/admin/tasks?state=blocked"
|
|
1303
|
+
|
|
1304
|
+
# Retry a parked task.
|
|
1305
|
+
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
1306
|
+
http://localhost:3000/v1/admin/tasks/7/retry
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1078
1309
|
## API sketch
|
|
1079
1310
|
|
|
1080
1311
|
- `GET /v1/meta` — version, extensions, health, auth posture
|
|
@@ -1084,6 +1315,8 @@ sigrun to apply" with no button. `GET /v1/meta` reports the posture
|
|
|
1084
1315
|
differs from boot) and PATCH reports `{applied, pending}`.
|
|
1085
1316
|
- `POST /v1/admin/restart` — graceful restart (admin, supervised
|
|
1086
1317
|
deploys only; see Restarting).
|
|
1318
|
+
- `GET /v1/admin/tasks?state=blocked`, `POST /v1/admin/tasks/{id}/retry`
|
|
1319
|
+
— parked-task inspection and recovery (admin; see Parked tasks).
|
|
1087
1320
|
- `GET /v1/jobs`, `POST /v1/jobs`, `PATCH`/`DELETE /v1/jobs/{id}` —
|
|
1088
1321
|
the jobs surface (see [Jobs](#jobs)); `POST /v1/jobs/{id}/run` fires
|
|
1089
1322
|
now; `GET /v1/jobs/{id}/runs` and `GET /v1/jobs/{id}/revisions` are
|
package/dist/cli.js
CHANGED
|
@@ -47,7 +47,9 @@ import { dirname, join } from "node:path";
|
|
|
47
47
|
import * as readline from "node:readline/promises";
|
|
48
48
|
import { Writable } from "node:stream";
|
|
49
49
|
import { resolveStoreRoots, writeFileAtomic } from "./harness/stores.js";
|
|
50
|
-
import { openIdentityStores } from "./harness/identity/index.js";
|
|
50
|
+
import { openIdentityStores, AccountLinkConflictError } from "./harness/identity/index.js";
|
|
51
|
+
import { normalizeTypedUsername } from "./harness/identity/typed-username.js";
|
|
52
|
+
import { deriveUsername, foldSubject } from "@sigrun/shared/usernames";
|
|
51
53
|
import { pathToFileURL } from "node:url";
|
|
52
54
|
import { effectiveMethods, loadConfig } from "./config.js";
|
|
53
55
|
import { probeIsolation, runHelperProbe, selfTestHelper } from "./sandbox/availability.js";
|
|
@@ -63,7 +65,9 @@ export const USAGE_TEXT = `usage: sigrun [--user <name>]
|
|
|
63
65
|
sigrun token create <name> --user <user>
|
|
64
66
|
sigrun token list
|
|
65
67
|
sigrun token revoke <id>
|
|
66
|
-
sigrun user add <user> [--admin] [--name <n>] [--email <e>]
|
|
68
|
+
sigrun user add [<user>] [--proxy <username>] [--admin] [--name <n>] [--email <e>]
|
|
69
|
+
sigrun user link <user> --proxy <username>
|
|
70
|
+
sigrun user unlink --proxy <username>
|
|
67
71
|
sigrun user list
|
|
68
72
|
sigrun provider list [--url <base>] [--token <token>]
|
|
69
73
|
sigrun provider key <id> [--url <base>] [--token <token>]
|
|
@@ -134,8 +138,9 @@ function parseTokenArgs(argv) {
|
|
|
134
138
|
throw usageError(`unknown token subcommand ${JSON.stringify(sub ?? "")} (create, list, revoke)`);
|
|
135
139
|
}
|
|
136
140
|
/**
|
|
137
|
-
* Parse `user <sub>
|
|
138
|
-
*
|
|
141
|
+
* Parse `user <sub> …`: `add [<user>] [--proxy <username>]`, explicit
|
|
142
|
+
* `link`/`unlink`, and `list`. A proxy add may omit the directory key,
|
|
143
|
+
* deriving it from the external username; ordinary add still requires it.
|
|
139
144
|
*/
|
|
140
145
|
function parseUserArgs(argv) {
|
|
141
146
|
const sub = argv[0];
|
|
@@ -145,51 +150,61 @@ function parseUserArgs(argv) {
|
|
|
145
150
|
}
|
|
146
151
|
return { command: "user", sub: "list" };
|
|
147
152
|
}
|
|
148
|
-
if (sub === "add") {
|
|
153
|
+
if (sub === "add" || sub === "link" || sub === "unlink") {
|
|
149
154
|
let user;
|
|
155
|
+
let proxy;
|
|
150
156
|
let admin = false;
|
|
151
157
|
let name;
|
|
152
158
|
let email;
|
|
153
159
|
let index = 1;
|
|
154
160
|
while (index < argv.length) {
|
|
155
161
|
const arg = argv[index];
|
|
156
|
-
if (arg === "--admin") {
|
|
162
|
+
if (sub === "add" && arg === "--admin") {
|
|
157
163
|
admin = true;
|
|
158
164
|
index += 1;
|
|
159
165
|
continue;
|
|
160
166
|
}
|
|
161
|
-
if (arg === "--name" || arg === "--email") {
|
|
167
|
+
if (arg === "--proxy" || (sub === "add" && (arg === "--name" || arg === "--email"))) {
|
|
162
168
|
const value = argv[index + 1];
|
|
163
169
|
if (value === undefined || value.startsWith("--")) {
|
|
164
170
|
throw usageError(`${arg} requires a value`);
|
|
165
171
|
}
|
|
166
|
-
if (arg === "--
|
|
172
|
+
if (arg === "--proxy")
|
|
173
|
+
proxy = value;
|
|
174
|
+
else if (arg === "--name")
|
|
167
175
|
name = value;
|
|
168
176
|
else
|
|
169
177
|
email = value;
|
|
170
178
|
index += 2;
|
|
171
179
|
continue;
|
|
172
180
|
}
|
|
173
|
-
if (user === undefined && !arg.startsWith("--")) {
|
|
181
|
+
if (sub !== "unlink" && user === undefined && !arg.startsWith("--")) {
|
|
174
182
|
user = arg;
|
|
175
183
|
index += 1;
|
|
176
184
|
continue;
|
|
177
185
|
}
|
|
178
186
|
throw usageError(`unexpected argument ${JSON.stringify(arg)}`);
|
|
179
187
|
}
|
|
180
|
-
if (
|
|
181
|
-
|
|
188
|
+
if (proxy === undefined) {
|
|
189
|
+
if (sub !== "add")
|
|
190
|
+
throw usageError(`user ${sub} requires --proxy <username>`);
|
|
191
|
+
if (user === undefined)
|
|
192
|
+
throw usageError("user add requires a <user>");
|
|
193
|
+
}
|
|
194
|
+
if (sub === "link" && user === undefined) {
|
|
195
|
+
throw usageError("user link requires a <user>");
|
|
182
196
|
}
|
|
183
197
|
return {
|
|
184
198
|
command: "user",
|
|
185
|
-
sub
|
|
186
|
-
user,
|
|
199
|
+
sub,
|
|
200
|
+
...(user === undefined ? {} : { user }),
|
|
201
|
+
...(proxy === undefined ? {} : { proxy }),
|
|
187
202
|
...(admin ? { admin } : {}),
|
|
188
203
|
...(name === undefined ? {} : { name }),
|
|
189
204
|
...(email === undefined ? {} : { email }),
|
|
190
205
|
};
|
|
191
206
|
}
|
|
192
|
-
throw usageError(`unknown user subcommand ${JSON.stringify(sub ?? "")} (add, list)`);
|
|
207
|
+
throw usageError(`unknown user subcommand ${JSON.stringify(sub ?? "")} (add, link, unlink, list)`);
|
|
193
208
|
}
|
|
194
209
|
/**
|
|
195
210
|
* Parse `provider <sub> …` (release 0.1 Task 2b): `list`, `key <id>`,
|
|
@@ -506,9 +521,21 @@ export async function ttyPrompt(question, opts) {
|
|
|
506
521
|
rl.close();
|
|
507
522
|
}
|
|
508
523
|
}
|
|
509
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Resolve the attach/run user: `--user` > `SIGRUN_USER` > tui.json. The
|
|
526
|
+
* source is operator-typed (unchecked): derive the canonical key before
|
|
527
|
+
* it reaches the wire (the server derives on receipt too). Unusable
|
|
528
|
+
* input fails loud, never a silent empty identity.
|
|
529
|
+
*/
|
|
510
530
|
function resolveUser(parsed, env, file) {
|
|
511
|
-
|
|
531
|
+
const raw = parsed.user ?? env.SIGRUN_USER ?? file.user;
|
|
532
|
+
if (raw === undefined || raw.trim() === "")
|
|
533
|
+
return undefined;
|
|
534
|
+
const derived = deriveUsername(raw);
|
|
535
|
+
if (derived === undefined) {
|
|
536
|
+
throw new Error(`invalid_username: "${raw}" derives no usable username`);
|
|
537
|
+
}
|
|
538
|
+
return derived;
|
|
512
539
|
}
|
|
513
540
|
/**
|
|
514
541
|
* The tui.json UI refresh fields (C1) as a spreadable launch fragment —
|
|
@@ -654,7 +681,14 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
654
681
|
if (parsed.command === "token") {
|
|
655
682
|
switch (parsed.sub) {
|
|
656
683
|
case "create": {
|
|
657
|
-
|
|
684
|
+
// A typed source, same rule as `user add`: the resulting key
|
|
685
|
+
// is what the token (and its printed secret) belongs to.
|
|
686
|
+
const raw = parsed.user;
|
|
687
|
+
const derived = deriveUsername(raw);
|
|
688
|
+
if (derived === undefined) {
|
|
689
|
+
throw new Error(`invalid_username: "${raw}" derives no usable username`);
|
|
690
|
+
}
|
|
691
|
+
const user = derived;
|
|
658
692
|
if (stores.users.get(user) === undefined) {
|
|
659
693
|
throw new Error(`unknown user "${user}" — add it first: sigrun user add ${user}`);
|
|
660
694
|
}
|
|
@@ -685,6 +719,13 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
685
719
|
}
|
|
686
720
|
switch (parsed.sub) {
|
|
687
721
|
case "add": {
|
|
722
|
+
const input = parsed.user ?? parsed.proxy;
|
|
723
|
+
const typed = normalizeTypedUsername(input, (key) => stores.users.get(key) !== undefined);
|
|
724
|
+
if (!typed.ok) {
|
|
725
|
+
throw new Error(typed.error === "invalid_username"
|
|
726
|
+
? `invalid_username: "${input}" derives no usable username`
|
|
727
|
+
: `username_taken: user "${parsed.user === undefined ? input : deriveUsername(input) ?? input}" already exists`);
|
|
728
|
+
}
|
|
688
729
|
const config = loadConfig(env, new SettingsStore({ dir: defaultConfigDir(env) }).read());
|
|
689
730
|
const coreRoot = selfPackageRoot();
|
|
690
731
|
const isolation = await isolationProbe({
|
|
@@ -703,13 +744,53 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
703
744
|
proxySecretHeader: config.auth.proxy.secretHeader,
|
|
704
745
|
}),
|
|
705
746
|
});
|
|
706
|
-
await stores.users.add({
|
|
707
|
-
user:
|
|
747
|
+
const record = await stores.users.add({
|
|
748
|
+
user: typed.key,
|
|
708
749
|
admin: parsed.admin ?? false,
|
|
709
750
|
...(parsed.name === undefined ? {} : { name: parsed.name }),
|
|
710
751
|
...(parsed.email === undefined ? {} : { email: parsed.email }),
|
|
711
752
|
source: "cli",
|
|
712
753
|
});
|
|
754
|
+
if (parsed.proxy !== undefined) {
|
|
755
|
+
const subject = foldSubject(parsed.proxy);
|
|
756
|
+
try {
|
|
757
|
+
await stores.accounts.link({
|
|
758
|
+
provider: "proxy",
|
|
759
|
+
subject,
|
|
760
|
+
user: record.user,
|
|
761
|
+
via: "admin",
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (!(error instanceof AccountLinkConflictError))
|
|
766
|
+
throw error;
|
|
767
|
+
await stores.users.remove(record.user);
|
|
768
|
+
const holder = stores.accounts.lookup("proxy", subject)?.user;
|
|
769
|
+
throw new Error(`account_link_conflict: proxy identity is linked to ${holder ?? "another user"}; removed user "${record.user}"`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
io.out?.(record.user);
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
case "link": {
|
|
776
|
+
const user = deriveUsername(parsed.user);
|
|
777
|
+
if (user === undefined || stores.users.get(user) === undefined) {
|
|
778
|
+
throw new Error(`unknown user "${parsed.user}"`);
|
|
779
|
+
}
|
|
780
|
+
const link = await stores.accounts.link({
|
|
781
|
+
provider: "proxy",
|
|
782
|
+
subject: foldSubject(parsed.proxy),
|
|
783
|
+
user,
|
|
784
|
+
via: "admin",
|
|
785
|
+
});
|
|
786
|
+
io.out?.(`${link.provider}:${link.subject}\t${link.user}`);
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
case "unlink": {
|
|
790
|
+
const subject = foldSubject(parsed.proxy);
|
|
791
|
+
if (!(await stores.accounts.unlink("proxy", subject))) {
|
|
792
|
+
throw new Error(`no proxy link for "${subject}"`);
|
|
793
|
+
}
|
|
713
794
|
return 0;
|
|
714
795
|
}
|
|
715
796
|
case "list": {
|
|
@@ -719,6 +800,8 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
719
800
|
record.user,
|
|
720
801
|
...(record.name === undefined ? [] : [record.name]),
|
|
721
802
|
...(record.email === undefined ? [] : [record.email]),
|
|
803
|
+
...stores.accounts.listForUser(record.user)
|
|
804
|
+
.map((link) => `${link.provider}:${link.subject}`),
|
|
722
805
|
].join("\t"));
|
|
723
806
|
}
|
|
724
807
|
return 0;
|
|
@@ -727,6 +810,15 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
727
810
|
// parseArgs guarantees one of the cases above.
|
|
728
811
|
throw new Error(`unhandled ${parsed.command} subcommand`);
|
|
729
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* The CLI-local mirror of the server's proxy-only rule (proxy-trust
|
|
815
|
+
* Task 6): no token callers exist, so a setup-code prompt can never
|
|
816
|
+
* be redeemed. Kept here rather than imported from src/server — the
|
|
817
|
+
* CLI does not import server auth modules.
|
|
818
|
+
*/
|
|
819
|
+
function isProxyOnly(methods) {
|
|
820
|
+
return methods.includes("proxy") && !methods.includes("token");
|
|
821
|
+
}
|
|
730
822
|
/**
|
|
731
823
|
* The pre-launch onboarding flow (Task 7, spec §5.8): `attach`/`run`
|
|
732
824
|
* ask the server its state; an UNCLAIMED server gets the interactive
|
|
@@ -766,6 +858,12 @@ async function onboardIfUnclaimed(server, prompt, fetchImpl) {
|
|
|
766
858
|
}
|
|
767
859
|
if (state.state !== "unclaimed")
|
|
768
860
|
return undefined;
|
|
861
|
+
// Proxy-trust Task 6: a proxy-only server without a vouched identity
|
|
862
|
+
// refuses the claim (403 proxy_identity_required) and never mints a
|
|
863
|
+
// setup code — prompt for one and the operator can only lose.
|
|
864
|
+
if (isProxyOnly(state.methods) && state.codeRequired && state.proxyIdentity === undefined) {
|
|
865
|
+
throw new Error("sigrun: this sigrun onboards through its sign-in proxy; open it in a browser");
|
|
866
|
+
}
|
|
769
867
|
let code;
|
|
770
868
|
if (state.codeRequired) {
|
|
771
869
|
code = (await prompt("setup code: ")).trim();
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isIanaTimeZone } from "./jobs/spec.js";
|
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { resolveStoreRoots } from "./harness/stores.js";
|
|
4
4
|
import { isLoopbackHost } from "./server/auth/loopback.js";
|
|
5
|
+
import { deriveUsername, isUsernameKey } from "@sigrun/shared/usernames";
|
|
5
6
|
/**
|
|
6
7
|
* Sigrun configuration.
|
|
7
8
|
*
|
|
@@ -432,6 +433,15 @@ function parseAuth(env, server) {
|
|
|
432
433
|
}
|
|
433
434
|
const token = env.SIGRUN_API_TOKEN?.trim() || undefined;
|
|
434
435
|
const secret = env.SIGRUN_PROXY_SECRET?.trim() || undefined;
|
|
436
|
+
// The service-token user is an operator-typed identity that no request
|
|
437
|
+
// ever re-derives: it must ALREADY be a canonical username key at load
|
|
438
|
+
// time (unlike the header/CLI sources, which derive). Anything else is
|
|
439
|
+
// a config error naming the derived suggestion.
|
|
440
|
+
const serviceTokenUser = env.SIGRUN_API_TOKEN_USER?.trim() || "sigrun";
|
|
441
|
+
if (token !== undefined && !isUsernameKey(serviceTokenUser)) {
|
|
442
|
+
throw new Error(`SIGRUN_API_TOKEN_USER must already be a username key, got "${serviceTokenUser}" — ` +
|
|
443
|
+
`did you mean "${deriveUsername(serviceTokenUser) ?? "sigrun"}"?`);
|
|
444
|
+
}
|
|
435
445
|
return {
|
|
436
446
|
sessionDays: server?.sessionDays ?? DEFAULT_AUTH.sessionDays,
|
|
437
447
|
methods,
|
|
@@ -459,7 +469,7 @@ function parseAuth(env, server) {
|
|
|
459
469
|
: {
|
|
460
470
|
serviceToken: {
|
|
461
471
|
token,
|
|
462
|
-
user:
|
|
472
|
+
user: serviceTokenUser,
|
|
463
473
|
},
|
|
464
474
|
}),
|
|
465
475
|
};
|
|
@@ -556,8 +566,13 @@ export function parseBackupSections(raw) {
|
|
|
556
566
|
function parseCompaction(env, server) {
|
|
557
567
|
// Env parse first (a bad env value fails loud even when the file's
|
|
558
568
|
// value is valid); a file field applies only where its env var is
|
|
559
|
-
// absent; code defaults last.
|
|
569
|
+
// absent; code defaults last. `modelOverrides` is file-only (a map
|
|
570
|
+
// has no env spelling), passed through verbatim — pi-durable's
|
|
571
|
+
// `compactionPolicy` applies it per model at compaction time.
|
|
560
572
|
return {
|
|
573
|
+
...(server?.modelOverrides === undefined
|
|
574
|
+
? {}
|
|
575
|
+
: { modelOverrides: server.modelOverrides }),
|
|
561
576
|
enabled: parseCompactionEnabled(env) ??
|
|
562
577
|
server?.enabled ??
|
|
563
578
|
DEFAULT_COMPACTION.enabled,
|
|
@@ -831,6 +846,7 @@ export function resolvedServerSection(config) {
|
|
|
831
846
|
* | `SIGRUN_PORT` | `port` |
|
|
832
847
|
* | `SIGRUN_WORKSPACE_PATH` | `workspacePath` |
|
|
833
848
|
* | `SIGRUN_STORAGE_PATH` | `storagePath` |
|
|
849
|
+
* | `SIGRUN_SKIP_MIGRATION_SNAPSHOT` | — (boot escape hatch; never persisted) |
|
|
834
850
|
* | `SIGRUN_DATA_DIR` | `dataDir` |
|
|
835
851
|
* | `SIGRUN_CACHE_DIR` | `cacheDir` |
|
|
836
852
|
* | `SIGRUN_STATE_DIR` | `stateDir` |
|
|
@@ -994,6 +1010,9 @@ export function effectiveServerSection(env, fileServer, config) {
|
|
|
994
1010
|
emit("compaction.reserveTokens", config.compaction.reserveTokens, ["compaction", "reserveTokens"]);
|
|
995
1011
|
emit("compaction.keepRecentTokens", config.compaction.keepRecentTokens, ["compaction", "keepRecentTokens"]);
|
|
996
1012
|
emit("compaction.backgroundTokens", config.compaction.backgroundTokens, ["compaction", "backgroundTokens"]);
|
|
1013
|
+
if (config.compaction.modelOverrides !== undefined) {
|
|
1014
|
+
emit("compaction.modelOverrides", config.compaction.modelOverrides, ["compaction", "modelOverrides"]);
|
|
1015
|
+
}
|
|
997
1016
|
emit("memorySeam.hookDeadlineMs", config.memorySeam.hookDeadlineMs, ["memorySeam", "hookDeadlineMs"]);
|
|
998
1017
|
emit("memorySeam.idleThresholdMs", config.memorySeam.idleThresholdMs, ["memorySeam", "idleThresholdMs"]);
|
|
999
1018
|
emit("memorySeam.workerDeadlineMs", config.memorySeam.workerDeadlineMs, ["memorySeam", "workerDeadlineMs"]);
|