@sigrun/sigrun 0.2.0 → 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 +145 -7
- package/README.md +333 -13
- package/dist/blocks/build-stamp.json +13 -0
- package/dist/blocks/builders.d.ts +57 -0
- package/dist/blocks/images.d.ts +26 -0
- package/dist/blocks/index.d.ts +20 -0
- package/dist/blocks/limits.d.ts +25 -0
- package/dist/blocks/placements.d.ts +26 -0
- package/dist/blocks/schema.d.ts +38 -0
- package/dist/blocks/validate.d.ts +73 -0
- package/dist/cli.js +117 -19
- package/dist/config.js +21 -2
- package/dist/extension.d.ts +48 -8
- package/dist/harness/ambient.js +2 -0
- package/dist/harness/blocks/state.js +1 -0
- package/dist/harness/boot.js +92 -6
- package/dist/harness/builtins.js +1 -1
- 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 +23 -1
- package/dist/harness/sections/context-files.js +1 -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/sandbox/availability.js +69 -20
- package/dist/sandbox/launch.js +11 -0
- package/dist/sandbox/policy.js +24 -3
- package/dist/sandbox/worker.js +4 -24
- package/dist/serve.js +162 -40
- package/dist/server/auth/ip.js +9 -4
- package/dist/server/auth/own-host.js +115 -0
- package/dist/server/auth/pipeline.js +93 -28
- 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/images/cache.js +13 -5
- package/dist/server/index.js +10 -2
- package/dist/server/login/resolve.js +88 -26
- package/dist/server/notifications.js +21 -4
- package/dist/server/routes/admin.js +97 -0
- package/dist/server/routes/images.js +16 -9
- package/dist/server/routes/login.js +8 -0
- package/dist/server/routes/me.js +1 -1
- package/dist/server/routes/meta.js +4 -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 +12 -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 +33 -4
- package/node_modules/@sigrun/shared/package.json +5 -1
- package/package.json +16 -15
- package/webui/dist/assets/{index-CxB969qp.js → index-CEOzj1C4.js} +11 -11
- package/webui/dist/index.html +1 -1
- package/CONTEXT.md +0 -163
- package/dist/server/login/usernames.js +0 -15
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed builders (spec §14.1): one function per block and field,
|
|
3
|
+
* plus `bind(pointer)` and `tree(root)`. Builders return plain JSON —
|
|
4
|
+
* there is no runtime besides them.
|
|
5
|
+
*
|
|
6
|
+
* Each builder takes its block's props object and returns the block
|
|
7
|
+
* with the `type` literal stamped in. The literal is spread LAST, so a
|
|
8
|
+
* stray `type` inside a JavaScript caller's props object can never
|
|
9
|
+
* override it; TypeScript callers cannot pass one at all (`Props` omits
|
|
10
|
+
* it). The tree BLOCK builder is exported as `treeBlock`, keeping the
|
|
11
|
+
* `tree` name for the version-stamping root wrapper.
|
|
12
|
+
*
|
|
13
|
+
* `tree(root)` stamps {@link BLOCK_SET_VERSION} from the shared wire
|
|
14
|
+
* package — the version the SERVER validates against, which is why
|
|
15
|
+
* extensions import these builders from `@sigrun/sigrun/blocks` (the
|
|
16
|
+
* host-linked copy, spec §14.1) instead of bundling their own.
|
|
17
|
+
*/
|
|
18
|
+
import type { Binding, Block, CalloutBlock, Card, ChartBlock, ChoiceField, CollectionBlock, ColumnsBlock, DateField, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, FieldsBlock, FileField, FormBlock, GridBlock, ImageBlock, ListField, LogBlock, LongtextField, MarkdownBlock, MeterBlock, NumberField, PageBlock, SectionBlock, SecretField, StackBlock, StatBlock, TextField, ToggleField, Tree, TreeBlock } from "@sigrun/shared/blocks";
|
|
19
|
+
/** One block/field's props: the wire shape minus the `type` literal. */
|
|
20
|
+
type Props<T> = Omit<T, "type">;
|
|
21
|
+
/** `bind("/cards")` → `{ $: "/cards" }` — a pointer into the surface's data model (§4.2). */
|
|
22
|
+
export declare function bind(pointer: string): Binding;
|
|
23
|
+
/** Wrap a root block with the server's block-set version (§4.4). */
|
|
24
|
+
export declare function tree(root: Block): Tree;
|
|
25
|
+
export declare function page(props: Props<PageBlock>): PageBlock;
|
|
26
|
+
export declare function stack(props: Props<StackBlock>): StackBlock;
|
|
27
|
+
export declare function columns(props: Props<ColumnsBlock>): ColumnsBlock;
|
|
28
|
+
export declare function grid(props: Props<GridBlock>): GridBlock;
|
|
29
|
+
export declare function section(props: Props<SectionBlock>): SectionBlock;
|
|
30
|
+
export declare function markdown(props: Props<MarkdownBlock>): MarkdownBlock;
|
|
31
|
+
export declare function fields(props: Props<FieldsBlock>): FieldsBlock;
|
|
32
|
+
export declare function stat(props: Props<StatBlock>): StatBlock;
|
|
33
|
+
export declare function callout(props: Props<CalloutBlock>): CalloutBlock;
|
|
34
|
+
export declare function image(props: Props<ImageBlock>): ImageBlock;
|
|
35
|
+
export declare function chart(props: Props<ChartBlock>): ChartBlock;
|
|
36
|
+
export declare function meter(props: Props<MeterBlock>): MeterBlock;
|
|
37
|
+
export declare function log(props: Props<LogBlock>): LogBlock;
|
|
38
|
+
/** The tree BLOCK builder (§5.3) — `tree` is the root wrapper. */
|
|
39
|
+
export declare function treeBlock(props: Props<TreeBlock>): TreeBlock;
|
|
40
|
+
export declare function diff(props: Props<DiffBlock>): DiffBlock;
|
|
41
|
+
export declare function details(props: Props<DetailsBlock>): DetailsBlock;
|
|
42
|
+
export declare function collection(props: Props<CollectionBlock>): CollectionBlock;
|
|
43
|
+
/** The collection item template (§5.5.1) — a slot bundle, not a block. */
|
|
44
|
+
export declare function card(props: Card): Card;
|
|
45
|
+
export declare function form(props: Props<FormBlock>): FormBlock;
|
|
46
|
+
export declare function text(props: Props<TextField>): TextField;
|
|
47
|
+
export declare function longtext(props: Props<LongtextField>): LongtextField;
|
|
48
|
+
export declare function number(props: Props<NumberField>): NumberField;
|
|
49
|
+
export declare function toggle(props: Props<ToggleField>): ToggleField;
|
|
50
|
+
export declare function choice(props: Props<ChoiceField>): ChoiceField;
|
|
51
|
+
export declare function date(props: Props<DateField>): DateField;
|
|
52
|
+
export declare function datetime(props: Props<DatetimeField>): DatetimeField;
|
|
53
|
+
export declare function daterange(props: Props<DaterangeField>): DaterangeField;
|
|
54
|
+
export declare function secret(props: Props<SecretField>): SecretField;
|
|
55
|
+
export declare function file(props: Props<FileField>): FileField;
|
|
56
|
+
export declare function list(props: Props<ListField>): ListField;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image source rules (spec §11.1): `asset:` paths, `data:` URLs of the
|
|
3
|
+
* four accepted MIME types (capped at 192 KiB of base64 text), and
|
|
4
|
+
* `https:` URLs. SVG and AVIF are never accepted (script payload; no
|
|
5
|
+
* TUI decoder), and `http:` URLs are rejected. The proxy that rewrites
|
|
6
|
+
* and fetches is U7; this module only validates.
|
|
7
|
+
*/
|
|
8
|
+
/** A source that passed the §11.1 rules. */
|
|
9
|
+
export type ImageSourceOk = {
|
|
10
|
+
ok: true;
|
|
11
|
+
kind: "asset" | "data" | "https";
|
|
12
|
+
};
|
|
13
|
+
/** A source the §11.1 rules reject. */
|
|
14
|
+
export type ImageSourceErr = {
|
|
15
|
+
ok: false;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
/** The result of checking one `src` value. */
|
|
19
|
+
export type ImageSourceResult = ImageSourceOk | ImageSourceErr;
|
|
20
|
+
/**
|
|
21
|
+
* Check one image `src` against the §11.1 source rules. Bindings
|
|
22
|
+
* (`{$: …}` objects) are not checked here — their resolved string is,
|
|
23
|
+
* where the data model holds one (the data-size limit already covers
|
|
24
|
+
* the byte cost).
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkImageSource(src: string): ImageSourceResult;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sigrun/sigrun/blocks` — the extension authoring surface for the
|
|
3
|
+
* block set (spec §14.1): the typed builders, the wire types, and
|
|
4
|
+
* `validateTree` (§14.2) so an extension can unit-test its trees
|
|
5
|
+
* against the same validation the server runs.
|
|
6
|
+
*
|
|
7
|
+
* Extensions import this subpath, never a bundled copy: the package is
|
|
8
|
+
* a `peerDependency: "*"` (see docs/extending.md) and
|
|
9
|
+
* `@sigrun/sigrun` is a HOST RUNTIME MODULE (src/harness/packages.ts)
|
|
10
|
+
* — the host links its own running copy into every extension packages
|
|
11
|
+
* dir, so `tree(...)` always stamps the server's
|
|
12
|
+
* {@link BLOCK_SET_VERSION}. The `exports` map points here at
|
|
13
|
+
* `dist/blocks/index.js` (never `src/`), which is what the built
|
|
14
|
+
* server ships.
|
|
15
|
+
*/
|
|
16
|
+
export * from "./builders.js";
|
|
17
|
+
export { validateTree, validateData, VALID_PLACEMENTS, PLACEMENT_RULES, LIMITS, } from "./validate.js";
|
|
18
|
+
export type { PlacementLimits, PlacementRule, ValidateDataResult, ValidateErr, ValidateOk, ValidateOptions, ValidateResult, ValidationError, } from "./validate.js";
|
|
19
|
+
export { BLOCK_SET_VERSION } from "@sigrun/shared/blocks";
|
|
20
|
+
export type * from "@sigrun/shared/blocks";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-placement size limits of spec §4.5: max blocks per tree, and
|
|
3
|
+
* max UTF-8 bytes of the serialized JSON for the tree and the data.
|
|
4
|
+
*
|
|
5
|
+
* Over a limit the server rejects with `413 surface_too_large` naming
|
|
6
|
+
* the limit; `validateTree`/`validateData` surface the same error
|
|
7
|
+
* strings. A collection counts as one block however many items it
|
|
8
|
+
* binds, and a `data:` image URL counts toward whichever of the tree or
|
|
9
|
+
* the data holds it (the sizes are measured over the serialized JSON,
|
|
10
|
+
* so both hold by construction).
|
|
11
|
+
*/
|
|
12
|
+
import type { Placement } from "@sigrun/shared/blocks";
|
|
13
|
+
/** One row of the §4.5 limits table. */
|
|
14
|
+
export interface PlacementLimits {
|
|
15
|
+
/** Max blocks in one tree (a collection counts as one). */
|
|
16
|
+
readonly blocks: number;
|
|
17
|
+
/** Max UTF-8 bytes of the serialized tree JSON. */
|
|
18
|
+
readonly treeBytes: number;
|
|
19
|
+
/** Max UTF-8 bytes of the serialized data JSON. */
|
|
20
|
+
readonly dataBytes: number;
|
|
21
|
+
}
|
|
22
|
+
/** Spec §4.5, one row per placement. */
|
|
23
|
+
export declare const LIMITS: Readonly<Record<Placement, PlacementLimits>>;
|
|
24
|
+
/** UTF-8 byte length of a value's serialized JSON (spec §4.5 sizing). */
|
|
25
|
+
export declare function jsonBytes(value: unknown): number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The placement rules of spec §5.7, as data: which blocks, which
|
|
3
|
+
* collection displays and which form fields each placement allows,
|
|
4
|
+
* plus the root-block and `page.sidebar` constraints the table carries.
|
|
5
|
+
*
|
|
6
|
+
* `validateTree` walks a tree against these rules; the tests pin the
|
|
7
|
+
* table's literal cells so this data cannot drift from the spec.
|
|
8
|
+
*/
|
|
9
|
+
/** One row of the §5.7 table. */
|
|
10
|
+
export interface PlacementRule {
|
|
11
|
+
/** Blocks allowed anywhere in the tree. */
|
|
12
|
+
readonly blocks: ReadonlySet<string>;
|
|
13
|
+
/** The only legal root block type, when the placement pins it. */
|
|
14
|
+
readonly rootBlock?: "page" | "form";
|
|
15
|
+
/** Whether a root `page` may carry `sidebar` (panel: no). */
|
|
16
|
+
readonly pageSidebar: boolean;
|
|
17
|
+
/** Collection displays allowed (`undefined`: all five). */
|
|
18
|
+
readonly collectionDisplays?: readonly string[];
|
|
19
|
+
/** Field types allowed in `form` blocks. */
|
|
20
|
+
readonly fields: ReadonlySet<string>;
|
|
21
|
+
}
|
|
22
|
+
/** The placement names, kept local so this module has no imports. */
|
|
23
|
+
type PlacementName = "view" | "panel" | "settings" | "chat" | "interaction" | "ambient";
|
|
24
|
+
/** Spec §5.7, one row per placement. */
|
|
25
|
+
export declare const PLACEMENT_RULES: Readonly<Record<PlacementName, PlacementRule>>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typebox wire schemas for every block and field (spec §5/§6.1),
|
|
3
|
+
* each compiled once into its own `Validator` (spec §14.2 validation).
|
|
4
|
+
*
|
|
5
|
+
* Design notes:
|
|
6
|
+
* - Schemas are the wire contract, so they deliberately do NOT set
|
|
7
|
+
* `additionalProperties: false`: unknown props pass through and
|
|
8
|
+
* channels ignore them (the block-set version governs what exists).
|
|
9
|
+
* - `Tone`, `IconName` and `Format` are enums/patterns built from the
|
|
10
|
+
* shared constants; an unknown icon name fails here, at the icon
|
|
11
|
+
* prop's own path.
|
|
12
|
+
* - Bindable props are `Union([literal, Binding])` throughout; the
|
|
13
|
+
* literal-ness of STRUCTURAL props (`display`, `groups`, `reorder`,
|
|
14
|
+
* `mode`, field `name`s, `columns`, handler names) is enforced by the
|
|
15
|
+
* second walk in `validate.ts`, not the schema, so the error can say
|
|
16
|
+
* "must be a literal" instead of an anyOf mismatch.
|
|
17
|
+
* - Child slots (`children`, `sidebar`, `main`, `detail`,
|
|
18
|
+
* `columns[].block`, `collection.item`, `form` children, a field's
|
|
19
|
+
* `list.item`, `page.switcher`, a bulk action's `form.root`) are
|
|
20
|
+
* deliberately loose here (any object): `validate.ts` walks the tree
|
|
21
|
+
* and validates every child node against ITS compiled validator, so
|
|
22
|
+
* one broken block is one precise error instead of a union mismatch
|
|
23
|
+
* per sibling branch.
|
|
24
|
+
* - `Compile` (typebox/compile) builds each validator; its errors are
|
|
25
|
+
* read from `instancePath`, relative to the node the validator ran
|
|
26
|
+
* against.
|
|
27
|
+
*/
|
|
28
|
+
import type { Validator } from "typebox/compile";
|
|
29
|
+
/** One compiled validator per block type, keyed by its `type` literal. */
|
|
30
|
+
export declare const BLOCK_VALIDATORS: ReadonlyMap<string, Validator>;
|
|
31
|
+
/** One compiled validator per field type, keyed by its `type` literal. */
|
|
32
|
+
export declare const FIELD_VALIDATORS: ReadonlyMap<string, Validator>;
|
|
33
|
+
/** The compiled validator for `Action` objects (spec §6.1). */
|
|
34
|
+
export declare const ACTION_VALIDATOR: Validator;
|
|
35
|
+
/** The compiled validator for action buttons (spec §6.1). */
|
|
36
|
+
export declare const ACTION_BUTTON_VALIDATOR: Validator;
|
|
37
|
+
/** The compiled validator for a collection's card template (spec §5.5.1). */
|
|
38
|
+
export declare const CARD_VALIDATOR: Validator;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validateTree` (spec §14.2): every error with its JSON path and a
|
|
3
|
+
* plain message.
|
|
4
|
+
*
|
|
5
|
+
* Two passes:
|
|
6
|
+
* 1. The compiled typebox validators (`schema.ts`) check each node's
|
|
7
|
+
* shape: the walk descends the tree, finds every block, field,
|
|
8
|
+
* action, button and card template, and runs the matching compiled
|
|
9
|
+
* validator against it. Instance paths (relative to the node) join
|
|
10
|
+
* the walk's path, so one broken block is one precise error
|
|
11
|
+
* (`root.main[2].text`), never a union mismatch per sibling.
|
|
12
|
+
* 2. The same walk checks what schemas cannot express: placements
|
|
13
|
+
* (§5.7), the literal-ness of structural props (§4.2), handler names
|
|
14
|
+
* (§6.1), relative bindings outside item templates (§4.2), ids
|
|
15
|
+
* (§4.1), `turn` outside chat (§6.2), `open` schemes (§6.1), image
|
|
16
|
+
* sources (§11.1), limits (§4.5), and the ambient one-line rule
|
|
17
|
+
* (§9.2).
|
|
18
|
+
*/
|
|
19
|
+
import { type Placement, type Tree } from "@sigrun/shared/blocks";
|
|
20
|
+
export { PLACEMENT_RULES } from "./placements.js";
|
|
21
|
+
export { LIMITS } from "./limits.js";
|
|
22
|
+
export type { PlacementRule } from "./placements.js";
|
|
23
|
+
export type { PlacementLimits } from "./limits.js";
|
|
24
|
+
/** The placements the block set defines (spec §3). */
|
|
25
|
+
export declare const VALID_PLACEMENTS: readonly Placement[];
|
|
26
|
+
/** One validation error: where and what (spec §14.2). */
|
|
27
|
+
export interface ValidationError {
|
|
28
|
+
path: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
/** Options for `validateTree`. */
|
|
32
|
+
export interface ValidateOptions {
|
|
33
|
+
placement: Placement;
|
|
34
|
+
/**
|
|
35
|
+
* The surface's `actions` map keys (spec §6.1); when given, every
|
|
36
|
+
* handler-name prop must be in it. When omitted (chat), handler
|
|
37
|
+
* names are free labels.
|
|
38
|
+
*/
|
|
39
|
+
handlers?: ReadonlySet<string>;
|
|
40
|
+
/** The server's block-set version; a mismatched tree is rejected (§4.4). */
|
|
41
|
+
serverV?: number;
|
|
42
|
+
/** For ambient trees: which `place` renders this body (§9.2 one-line rule). */
|
|
43
|
+
ambientPlace?: "status" | "footer" | "panel";
|
|
44
|
+
}
|
|
45
|
+
/** The ok result of `validateTree`: the tree, narrowed to the wire type. */
|
|
46
|
+
export interface ValidateOk {
|
|
47
|
+
ok: true;
|
|
48
|
+
tree: Tree;
|
|
49
|
+
}
|
|
50
|
+
/** The failure result: every error found, each with its path. */
|
|
51
|
+
export interface ValidateErr {
|
|
52
|
+
ok: false;
|
|
53
|
+
errors: ValidationError[];
|
|
54
|
+
}
|
|
55
|
+
/** `validateTree`'s result. */
|
|
56
|
+
export type ValidateResult = ValidateOk | ValidateErr;
|
|
57
|
+
/** The result of `validateData`. */
|
|
58
|
+
export type ValidateDataResult = {
|
|
59
|
+
ok: true;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
errors: ValidationError[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Validate a block tree against a placement (spec §14.2). Returns every
|
|
66
|
+
* error with its JSON path; on success, the narrowed tree.
|
|
67
|
+
*/
|
|
68
|
+
export declare function validateTree(tree: unknown, opts: ValidateOptions): ValidateResult;
|
|
69
|
+
/**
|
|
70
|
+
* Validate a surface's data object: a JSON object within the placement's
|
|
71
|
+
* data-size limit (spec §4.5).
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateData(data: unknown, placement: Placement): ValidateDataResult;
|
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"]);
|