@crvouga/atlas 1.12.0 → 1.14.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 +13 -1
- package/dist/critics/src/contract.d.ts +113 -0
- package/dist/critics/src/contract.js +48 -0
- package/dist/critics/src/credentials.d.ts +72 -0
- package/dist/critics/src/credentials.js +136 -0
- package/dist/critics/src/critique.d.ts +128 -0
- package/dist/critics/src/critique.js +163 -0
- package/dist/critics/src/index.d.ts +21 -0
- package/dist/critics/src/index.js +21 -0
- package/dist/critics/src/judging.d.ts +153 -0
- package/dist/critics/src/judging.js +145 -0
- package/dist/critics/src/publish.d.ts +23 -0
- package/dist/critics/src/publish.js +21 -0
- package/dist/critics/src/runner.d.ts +104 -0
- package/dist/critics/src/runner.js +202 -0
- package/dist/critics/src/scope.d.ts +8 -0
- package/dist/critics/src/scope.js +26 -0
- package/dist/playwright/src/index.d.ts +4 -0
- package/dist/playwright/src/index.js +4 -0
- package/dist/playwright/src/ui-catalog.d.ts +51 -0
- package/dist/playwright/src/ui-catalog.js +44 -0
- package/dist/reports/src/compose.d.ts +2 -2
- package/dist/reports/src/compose.js +6 -5
- package/dist/reports/src/evidence-types.d.ts +3 -0
- package/dist/reports/src/evidence-view.d.ts +2 -1
- package/dist/reports/src/evidence-view.js +5 -2
- package/dist/reports/src/layer-merge.d.ts +16 -4
- package/dist/reports/src/layer-merge.js +23 -5
- package/dist/reports/src/report-index.js +9 -5
- package/dist/reports/src/report-status.d.ts +1 -1
- package/dist/reports/src/report-status.js +6 -4
- package/dist/reports/src/ui-catalog-publish.d.ts +92 -0
- package/dist/reports/src/ui-catalog-publish.js +68 -0
- package/dist/reports/src/ui-catalog.d.ts +229 -0
- package/dist/reports/src/ui-catalog.js +273 -0
- package/dist/reports/src/ui-state.d.ts +70 -0
- package/dist/reports/src/ui-state.js +66 -0
- package/dist/schema/src/api.d.ts +351 -0
- package/dist/schema/src/api.js +72 -1
- package/dist/schema/src/critique.d.ts +144 -0
- package/dist/schema/src/critique.js +96 -0
- package/dist/schema/src/index.d.ts +2 -1
- package/dist/schema/src/index.js +2 -1
- package/dist/schema/src/server-config.d.ts +7 -1
- package/dist/schema/src/server-config.js +6 -3
- package/dist/server/src/app.js +15 -0
- package/dist/server/src/auth.d.ts +1 -1
- package/dist/server/src/auth.js +2 -2
- package/dist/server/src/config.js +2 -2
- package/dist/server/src/openapi.d.ts +3 -3
- package/dist/server/src/openapi.js +29 -6
- package/dist/server/src/store-api.js +39 -1
- package/docs/agents.md +1 -0
- package/docs/critics.md +190 -0
- package/docs/extending.md +1 -0
- package/docs/reference/critics.md +840 -0
- package/docs/reference/http-api.md +52 -2
- package/docs/reference/playwright.md +63 -0
- package/docs/reference/reports.md +608 -8
- package/docs/reference/schema.md +152 -2
- package/docs/reference/server.md +3 -3
- package/docs/report-contract.md +6 -2
- package/docs/rich-evidence.md +5 -0
- package/docs/server.md +8 -4
- package/docs/ui-catalog.md +173 -0
- package/docs/ui.md +15 -0
- package/llms-full.txt +457 -8
- package/llms.txt +3 -0
- package/package.json +8 -2
- package/packages/critics/README.md +18 -0
- package/packages/schema/schemas/critique.schema.json +254 -0
- package/packages/schema/schemas/effective-delta.schema.json +73 -0
- package/packages/schema/schemas/effective-evidence.schema.json +73 -0
- package/packages/schema/schemas/layer-rows.schema.json +73 -0
- package/packages/schema/schemas/server-config.schema.json +2 -1
- package/packages/schema/schemas/server-info.schema.json +1 -0
- package/packages/server/openapi.json +1443 -80
- package/packages/visualizer/src/app/AppShell.tsx +36 -30
- package/packages/visualizer/src/app/router.tsx +10 -1
- package/packages/visualizer/src/catalog/UiCatalogFilters.tsx +65 -0
- package/packages/visualizer/src/catalog/UiCatalogPage.tsx +342 -0
- package/packages/visualizer/src/catalog/provider.tsx +25 -0
- package/packages/visualizer/src/catalog/search.ts +68 -0
- package/packages/visualizer/src/catalog/words.ts +26 -0
- package/packages/visualizer/src/components/Header.tsx +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.14.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add a UI catalog of captured states that streams, shards and is independent of the collector ([81b2938](https://github.com/crvouga/atlas/commit/81b29388b4cb77ca8c3763aac969d23436cd18dd))
|
|
8
|
+
|
|
9
|
+
## 1.13.0 (2026-10-28)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- add the critic contract, the atlas.critique evidence, a runner and a scoped critic role ([38fdae2](https://github.com/crvouga/atlas/commit/38fdae29e2ece1b57c246140fb0e12a16d01ec25))
|
|
14
|
+
|
|
15
|
+
## 1.12.0 (2026-10-28)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
6
18
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module The critic contract
|
|
3
|
+
*
|
|
4
|
+
* A critic is an automated reviewer of the UI catalog: it is handed one captured UI (an image and what is known about
|
|
5
|
+
* it) and answers with a judgement. Model-based, LLM-based and classic ML or heuristic critics are the same thing
|
|
6
|
+
* here, with no kind privileged: the contract is `describe`, optionally `setup`, and `evaluate` of one image. The
|
|
7
|
+
* runner (`runCritic`) owns everything else: concurrency, retries, rate limits, budgets, caching, publishing and
|
|
8
|
+
* progress. A critic never talks to Atlas, never publishes, and never sees a credential it was not configured with.
|
|
9
|
+
*
|
|
10
|
+
* Critics are producers that publish layers. Atlas never executes one inside the server.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/critics.md
|
|
13
|
+
*/
|
|
14
|
+
import type { Critique } from '../../schema/src/critique.js';
|
|
15
|
+
/** One captured UI state of the catalog, as a critic is told about it. */
|
|
16
|
+
export type CatalogEntry = {
|
|
17
|
+
/** The entry's stable identity in the catalog. */
|
|
18
|
+
readonly key: string;
|
|
19
|
+
/** What a person calls it. */
|
|
20
|
+
readonly title: string;
|
|
21
|
+
/** The route or screen it shows, when known. */
|
|
22
|
+
readonly route?: string;
|
|
23
|
+
/** The kind of UI state: `screen`, `overlay`, `loading`, `error`, `empty`, or anything else the catalog declares. */
|
|
24
|
+
readonly state: string;
|
|
25
|
+
/** The device or browser profile it was captured with, in whatever shape the catalog records it. */
|
|
26
|
+
readonly profile?: unknown;
|
|
27
|
+
};
|
|
28
|
+
/** An entry to judge together with the hash of the image bytes that were stored for it. */
|
|
29
|
+
export type JudgeableEntry = CatalogEntry & {
|
|
30
|
+
/** SHA-256 of the exact image bytes, lowercase hex. A critique is anchored to it, and an identical image is never judged twice. */
|
|
31
|
+
readonly imageSha256: string;
|
|
32
|
+
};
|
|
33
|
+
/** What a critic says about itself before it runs, so a person can decide whether to run it. */
|
|
34
|
+
export type CriticDescription = {
|
|
35
|
+
/** Where images are sent: a host or provider name, or `null` when they never leave the machine. A run that sends images to a third party needs explicit confirmation. */
|
|
36
|
+
readonly sendsImagesTo: string | null;
|
|
37
|
+
/** The model that judges, when there is one. Part of what makes two critiques the same. */
|
|
38
|
+
readonly model?: {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly provider?: string;
|
|
41
|
+
};
|
|
42
|
+
/** Everything else that decides the answer and is fixed for the run: temperature, seed, the SHA-256 of the rubric. Part of what makes two critiques the same. Never put a credential here. */
|
|
43
|
+
readonly parameters?: Readonly<Record<string, unknown>>;
|
|
44
|
+
/** Whether `setup` must run first (for example to download and verify a checkpoint). */
|
|
45
|
+
readonly needsSetup: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** What `evaluate` is handed: the exact stored bytes, their hash, the entry and a signal that fires on timeout or cancellation. */
|
|
48
|
+
export type CriticInput = {
|
|
49
|
+
/** The exact bytes that were stored. */
|
|
50
|
+
readonly image: Uint8Array;
|
|
51
|
+
/** Their SHA-256, lowercase hex. */
|
|
52
|
+
readonly imageSha256: string;
|
|
53
|
+
/** What is known about the UI. */
|
|
54
|
+
readonly entry: CatalogEntry;
|
|
55
|
+
/** Aborted when the call times out or the run is cancelled. A critic that can stop early should. */
|
|
56
|
+
readonly signal: AbortSignal;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* What `evaluate` returns: a critique without its `schema`, `subject`, `critic`, `model` and `parameters`, which the
|
|
60
|
+
* runner fills in from the critic and the entry so they cannot disagree with what was run. A critic that has only a
|
|
61
|
+
* score returns `{ verdict: { score } }`; a richer one adds `findings`.
|
|
62
|
+
*/
|
|
63
|
+
export type CriticAnswer = Pick<Critique, 'verdict' | 'findings' | 'usage'>;
|
|
64
|
+
/**
|
|
65
|
+
* A critic. `evaluate` judges one image and may throw: throw a `CriticError` subclass (`RateLimitedError`,
|
|
66
|
+
* `InvalidOutputError`) to say why, and the runner records the failure and retries it on the next run. It never
|
|
67
|
+
* becomes a score.
|
|
68
|
+
*/
|
|
69
|
+
export type Critic = {
|
|
70
|
+
/** Short and stable: `uiclip`, `vision-llm`. With the version it names the critic in every critique and, as `critic.<name>`, the producer of its layers. */
|
|
71
|
+
readonly name: string;
|
|
72
|
+
/** Of the critic's own logic. A newer version replaces an older one's critiques and is judged again. */
|
|
73
|
+
readonly version: string;
|
|
74
|
+
/** Open vocabulary: `model`, `llm`, `ml`, `heuristic`, or anything else. */
|
|
75
|
+
readonly kind?: string;
|
|
76
|
+
/** Where images go, which model, which fixed parameters, whether setup is needed. */
|
|
77
|
+
describe(): CriticDescription;
|
|
78
|
+
/** Provisions what the critic needs, such as downloading and verifying a checkpoint. Inference never downloads anything. */
|
|
79
|
+
setup?(): Promise<void>;
|
|
80
|
+
/** Judges one image. */
|
|
81
|
+
evaluate(input: CriticInput): Promise<CriticAnswer>;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Why a critic could not produce a verdict. `reason` is a code in an open vocabulary and is what is recorded in the
|
|
85
|
+
* critique (`failure.reason`); the message is recorded too, after credentials are redacted from it.
|
|
86
|
+
*/
|
|
87
|
+
export declare class CriticError extends Error {
|
|
88
|
+
readonly reason: string;
|
|
89
|
+
/** @param reason the code recorded in the critique, such as `error`, `timeout`, `rate-limited`, `invalid-output`. */
|
|
90
|
+
constructor(reason: string, message: string);
|
|
91
|
+
}
|
|
92
|
+
/** A provider answered that it is rate limited. `retryAfterMs` is its `Retry-After`, when it said: the runner waits at least that long before trying again. */
|
|
93
|
+
export declare class RateLimitedError extends CriticError {
|
|
94
|
+
readonly retryAfterMs?: number | undefined;
|
|
95
|
+
/** @param retryAfterMs milliseconds the provider asked to wait, when it said. */
|
|
96
|
+
constructor(message: string, retryAfterMs?: number | undefined);
|
|
97
|
+
}
|
|
98
|
+
/** A provider answered with something that is not the structure that was asked for. The entry is recorded as failed and no score is invented. */
|
|
99
|
+
export declare class InvalidOutputError extends CriticError {
|
|
100
|
+
constructor(message: string);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Checks a critic's shape and returns it unchanged, so a typo is found where the critic is defined and not in the middle
|
|
104
|
+
* of a run. Throws when the name is not made of lowercase letters, digits, `-` and `.`, or when the version is empty.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* export default defineCritic({
|
|
108
|
+
* name: 'contrast', version: '1.0.0', kind: 'heuristic',
|
|
109
|
+
* describe: () => ({ sendsImagesTo: null, needsSetup: false }),
|
|
110
|
+
* evaluate: async ({ image }) => ({ verdict: { score: measureContrast(image), scale: '0..1, higher is better' } })
|
|
111
|
+
* });
|
|
112
|
+
*/
|
|
113
|
+
export declare function defineCritic<T extends Critic>(critic: T): T;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a critic could not produce a verdict. `reason` is a code in an open vocabulary and is what is recorded in the
|
|
3
|
+
* critique (`failure.reason`); the message is recorded too, after credentials are redacted from it.
|
|
4
|
+
*/
|
|
5
|
+
export class CriticError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
/** @param reason the code recorded in the critique, such as `error`, `timeout`, `rate-limited`, `invalid-output`. */
|
|
8
|
+
constructor(reason, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
this.name = 'CriticError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** A provider answered that it is rate limited. `retryAfterMs` is its `Retry-After`, when it said: the runner waits at least that long before trying again. */
|
|
15
|
+
export class RateLimitedError extends CriticError {
|
|
16
|
+
retryAfterMs;
|
|
17
|
+
/** @param retryAfterMs milliseconds the provider asked to wait, when it said. */
|
|
18
|
+
constructor(message, retryAfterMs) {
|
|
19
|
+
super('rate-limited', message);
|
|
20
|
+
this.retryAfterMs = retryAfterMs;
|
|
21
|
+
this.name = 'RateLimitedError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** A provider answered with something that is not the structure that was asked for. The entry is recorded as failed and no score is invented. */
|
|
25
|
+
export class InvalidOutputError extends CriticError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super('invalid-output', message);
|
|
28
|
+
this.name = 'InvalidOutputError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Checks a critic's shape and returns it unchanged, so a typo is found where the critic is defined and not in the middle
|
|
33
|
+
* of a run. Throws when the name is not made of lowercase letters, digits, `-` and `.`, or when the version is empty.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* export default defineCritic({
|
|
37
|
+
* name: 'contrast', version: '1.0.0', kind: 'heuristic',
|
|
38
|
+
* describe: () => ({ sendsImagesTo: null, needsSetup: false }),
|
|
39
|
+
* evaluate: async ({ image }) => ({ verdict: { score: measureContrast(image), scale: '0..1, higher is better' } })
|
|
40
|
+
* });
|
|
41
|
+
*/
|
|
42
|
+
export function defineCritic(critic) {
|
|
43
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/.test(critic.name))
|
|
44
|
+
throw new Error(`Invalid critic name "${critic.name}": use lowercase letters, digits, "-" and "."`);
|
|
45
|
+
if (!critic.version)
|
|
46
|
+
throw new Error(`Critic "${critic.name}" has no version`);
|
|
47
|
+
return critic;
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Credentials for bring-your-own-AI critics
|
|
3
|
+
*
|
|
4
|
+
* Configuration names a credential by reference (`env:OPENAI_API_KEY`, `file:~/.config/key`) and never contains one,
|
|
5
|
+
* so it can be committed. A credential is read at run time and never written to a layer, a log, an error message, a
|
|
6
|
+
* progress display, a cache file or JSON output: everything a run writes goes through a redactor built from the
|
|
7
|
+
* credentials the run holds. A configuration that contains a literal credential is refused with a message that names
|
|
8
|
+
* where it is and never repeats it.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/critics.md#credentials
|
|
11
|
+
*/
|
|
12
|
+
/** A credential by reference: the environment variable that holds it, or the file that does. */
|
|
13
|
+
export type CredentialRef = {
|
|
14
|
+
readonly scheme: 'env';
|
|
15
|
+
readonly name: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly scheme: 'file';
|
|
18
|
+
readonly path: string;
|
|
19
|
+
};
|
|
20
|
+
/** The text that stands for a credential in anything written. */
|
|
21
|
+
export declare const REDACTED = "[redacted]";
|
|
22
|
+
/**
|
|
23
|
+
* Reads a reference such as `env:OPENAI_API_KEY` or `file:/run/secrets/key`.
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when the text is not a reference (so it may be a literal credential, which configuration must not
|
|
26
|
+
* contain).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* parseCredentialRef('env:SOME_API_KEY'); // { scheme: 'env', name: 'SOME_API_KEY' }
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseCredentialRef(text: string): CredentialRef | null;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves a reference to the credential it names, at run time.
|
|
34
|
+
*
|
|
35
|
+
* Throws, naming the reference and never a value, when the text is not a reference, the variable is unset or empty, or
|
|
36
|
+
* the file cannot be read or is empty. A trailing newline in a file is not part of the credential.
|
|
37
|
+
*
|
|
38
|
+
* @param ref the reference from configuration.
|
|
39
|
+
* @param source where to read from; the defaults are the process environment and the file system. Tests pass fakes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveCredential(ref: string, source?: {
|
|
42
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
43
|
+
readonly readFile?: (path: string) => Promise<string>;
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
/** A place in a configuration that holds a literal credential: where, and why it was judged one. Never the value. */
|
|
46
|
+
export type LiteralCredential = {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Finds literal credentials in a configuration: a string under a key named like a credential (`apiKey`, `token`,
|
|
52
|
+
* `secret`…) that is not a reference, and any string anywhere that has the shape of a provider key, token or JWT.
|
|
53
|
+
* A key whose name ends in `Ref` or `Env`, or whose value is a reference, is fine.
|
|
54
|
+
*
|
|
55
|
+
* Returns the findings in document order; the value of a finding is never included.
|
|
56
|
+
*/
|
|
57
|
+
export declare function findLiteralCredentials(config: unknown, path?: string): LiteralCredential[];
|
|
58
|
+
/**
|
|
59
|
+
* Refuses a configuration that contains a literal credential. Throws one error that lists every place, with the reason
|
|
60
|
+
* and never the value.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertNoLiteralCredentials(config: unknown): void;
|
|
63
|
+
/** Replaces credentials in text with {@link REDACTED}. */
|
|
64
|
+
export type Redactor = (text: string) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Builds a redactor for the credentials a run holds. It removes each secret (and its URL-encoded and base64 forms) and
|
|
67
|
+
* anything shaped like a provider key, token or JWT. Secrets shorter than 6 characters are ignored, because replacing
|
|
68
|
+
* them would damage ordinary text and they are not credentials.
|
|
69
|
+
*
|
|
70
|
+
* @param secrets the resolved credentials of the run.
|
|
71
|
+
*/
|
|
72
|
+
export declare function createRedactor(secrets: readonly string[]): Redactor;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Credentials for bring-your-own-AI critics
|
|
3
|
+
*
|
|
4
|
+
* Configuration names a credential by reference (`env:OPENAI_API_KEY`, `file:~/.config/key`) and never contains one,
|
|
5
|
+
* so it can be committed. A credential is read at run time and never written to a layer, a log, an error message, a
|
|
6
|
+
* progress display, a cache file or JSON output: everything a run writes goes through a redactor built from the
|
|
7
|
+
* credentials the run holds. A configuration that contains a literal credential is refused with a message that names
|
|
8
|
+
* where it is and never repeats it.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/critics.md#credentials
|
|
11
|
+
*/
|
|
12
|
+
/** The text that stands for a credential in anything written. */
|
|
13
|
+
export const REDACTED = '[redacted]';
|
|
14
|
+
/**
|
|
15
|
+
* Reads a reference such as `env:OPENAI_API_KEY` or `file:/run/secrets/key`.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when the text is not a reference (so it may be a literal credential, which configuration must not
|
|
18
|
+
* contain).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* parseCredentialRef('env:SOME_API_KEY'); // { scheme: 'env', name: 'SOME_API_KEY' }
|
|
22
|
+
*/
|
|
23
|
+
export function parseCredentialRef(text) {
|
|
24
|
+
const env = /^env:([A-Za-z_][A-Za-z0-9_]*)$/.exec(text);
|
|
25
|
+
if (env)
|
|
26
|
+
return { scheme: 'env', name: env[1] };
|
|
27
|
+
const file = /^file:(.+)$/.exec(text);
|
|
28
|
+
if (file)
|
|
29
|
+
return { scheme: 'file', path: file[1] };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves a reference to the credential it names, at run time.
|
|
34
|
+
*
|
|
35
|
+
* Throws, naming the reference and never a value, when the text is not a reference, the variable is unset or empty, or
|
|
36
|
+
* the file cannot be read or is empty. A trailing newline in a file is not part of the credential.
|
|
37
|
+
*
|
|
38
|
+
* @param ref the reference from configuration.
|
|
39
|
+
* @param source where to read from; the defaults are the process environment and the file system. Tests pass fakes.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveCredential(ref, source = {}) {
|
|
42
|
+
const parsed = parseCredentialRef(ref);
|
|
43
|
+
if (!parsed)
|
|
44
|
+
throw new Error('A credential must be given as a reference such as env:NAME or file:PATH');
|
|
45
|
+
if (parsed.scheme === 'env') {
|
|
46
|
+
const value = (source.env ?? process.env)[parsed.name];
|
|
47
|
+
if (!value)
|
|
48
|
+
throw new Error(`The credential ${ref} is not set: export ${parsed.name}`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = await (source.readFile ?? (async (path) => (await import('node:fs/promises')).readFile(path, 'utf8')))(parsed.path);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`The credential ${ref} could not be read`);
|
|
57
|
+
}
|
|
58
|
+
const value = text.replace(/\r?\n$/, '');
|
|
59
|
+
if (!value)
|
|
60
|
+
throw new Error(`The credential ${ref} is empty`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/** Shapes that are credentials whatever they are called: provider keys, access tokens, bearer tokens, JWTs. */
|
|
64
|
+
const LOOKS_LIKE_A_CREDENTIAL = [
|
|
65
|
+
/\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}/,
|
|
66
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/,
|
|
67
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
68
|
+
/\bxox[abpr]-[A-Za-z0-9-]{10,}/,
|
|
69
|
+
/\bAIza[0-9A-Za-z_-]{30,}/,
|
|
70
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
|
|
71
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i
|
|
72
|
+
];
|
|
73
|
+
const CREDENTIAL_NAME = /(?:key|token|secret|password|passwd|credential|authorization|bearer)/i;
|
|
74
|
+
/**
|
|
75
|
+
* Finds literal credentials in a configuration: a string under a key named like a credential (`apiKey`, `token`,
|
|
76
|
+
* `secret`…) that is not a reference, and any string anywhere that has the shape of a provider key, token or JWT.
|
|
77
|
+
* A key whose name ends in `Ref` or `Env`, or whose value is a reference, is fine.
|
|
78
|
+
*
|
|
79
|
+
* Returns the findings in document order; the value of a finding is never included.
|
|
80
|
+
*/
|
|
81
|
+
export function findLiteralCredentials(config, path = '') {
|
|
82
|
+
const found = [];
|
|
83
|
+
const walk = (value, where, key) => {
|
|
84
|
+
if (typeof value === 'string') {
|
|
85
|
+
if (parseCredentialRef(value))
|
|
86
|
+
return;
|
|
87
|
+
if (CREDENTIAL_NAME.test(key) && !/(?:ref|env)$/i.test(key) && value !== '')
|
|
88
|
+
found.push({ path: where, reason: `"${key}" holds a literal value: name the credential by reference instead (env:NAME or file:PATH)` });
|
|
89
|
+
else if (LOOKS_LIKE_A_CREDENTIAL.some(pattern => pattern.test(value)))
|
|
90
|
+
found.push({ path: where, reason: 'the value has the shape of a credential: name it by reference instead (env:NAME or file:PATH)' });
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(value))
|
|
93
|
+
value.forEach((item, index) => walk(item, `${where}[${index}]`, key));
|
|
94
|
+
else if (value !== null && typeof value === 'object')
|
|
95
|
+
for (const [name, item] of Object.entries(value))
|
|
96
|
+
walk(item, where ? `${where}.${name}` : name, name);
|
|
97
|
+
};
|
|
98
|
+
walk(config, path, '');
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Refuses a configuration that contains a literal credential. Throws one error that lists every place, with the reason
|
|
103
|
+
* and never the value.
|
|
104
|
+
*/
|
|
105
|
+
export function assertNoLiteralCredentials(config) {
|
|
106
|
+
const found = findLiteralCredentials(config);
|
|
107
|
+
if (found.length)
|
|
108
|
+
throw new Error(`The configuration contains ${found.length === 1 ? 'a literal credential' : 'literal credentials'}, which must never be committed or written:\n${found.map(item => ` ${item.path || '(root)'}: ${item.reason}`).join('\n')}`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds a redactor for the credentials a run holds. It removes each secret (and its URL-encoded and base64 forms) and
|
|
112
|
+
* anything shaped like a provider key, token or JWT. Secrets shorter than 6 characters are ignored, because replacing
|
|
113
|
+
* them would damage ordinary text and they are not credentials.
|
|
114
|
+
*
|
|
115
|
+
* @param secrets the resolved credentials of the run.
|
|
116
|
+
*/
|
|
117
|
+
export function createRedactor(secrets) {
|
|
118
|
+
const forms = new Set();
|
|
119
|
+
for (const secret of secrets) {
|
|
120
|
+
if (secret.length < 6)
|
|
121
|
+
continue;
|
|
122
|
+
forms.add(secret);
|
|
123
|
+
forms.add(encodeURIComponent(secret));
|
|
124
|
+
forms.add(Buffer.from(secret).toString('base64'));
|
|
125
|
+
}
|
|
126
|
+
// Longest first, so a secret that contains another is removed whole.
|
|
127
|
+
const ordered = [...forms].sort((a, b) => b.length - a.length);
|
|
128
|
+
return text => {
|
|
129
|
+
let out = text;
|
|
130
|
+
for (const form of ordered)
|
|
131
|
+
out = out.split(form).join(REDACTED);
|
|
132
|
+
for (const pattern of LOOKS_LIKE_A_CREDENTIAL)
|
|
133
|
+
out = out.replace(new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`), REDACTED);
|
|
134
|
+
return out;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Critiques as layers
|
|
3
|
+
*
|
|
4
|
+
* Turns a critic's judgements into the checks of a layer (`critiqueChecks`, `critiqueLayer`) and reads them back
|
|
5
|
+
* (`readCritique`, `critiquesOfLayer`). A critique is a check in a part of kind `critique` whose `status` says whether
|
|
6
|
+
* the critic ran; the opinion is in `extensions["atlas.critique"]`, and is repeated in standard evidence (a
|
|
7
|
+
* measurement for the score, an annotation for each finding) so a viewer that has never heard of critics still shows
|
|
8
|
+
* it. Critiques are anchored to the image hash they judged: when an entry's image changes, an older critique is still
|
|
9
|
+
* shown, as judging an earlier image, and the entry counts as not yet judged (`critiqueCurrency`).
|
|
10
|
+
*
|
|
11
|
+
* @see docs/critics.md
|
|
12
|
+
* @see docs/extending.md
|
|
13
|
+
*/
|
|
14
|
+
import { type Critique } from '../../schema/src/critique.js';
|
|
15
|
+
import type { PublishChecksInput } from '../../schema/src/layer.js';
|
|
16
|
+
import type { Critic, CriticDescription, JudgeableEntry } from './contract.js';
|
|
17
|
+
/** The prefix of the id of every critique check: `critique/<entry key>`. It keeps a critique from ever sharing a slot with a test. */
|
|
18
|
+
export declare const CRITIQUE_CHECK_PREFIX = "critique/";
|
|
19
|
+
/** The result of judging one entry: a critique, or the reason there is none. Either way it has what it judged. */
|
|
20
|
+
export type CritiqueOutcome = {
|
|
21
|
+
/** The entry and the image that was judged. */
|
|
22
|
+
readonly entry: JudgeableEntry;
|
|
23
|
+
/** The critique with its subject, critic and verdict (or failure) filled in. */
|
|
24
|
+
readonly critique: Critique;
|
|
25
|
+
};
|
|
26
|
+
/** The check ids and labels one critique is published under. */
|
|
27
|
+
export declare function critiqueCheckId(entryKey: string): string;
|
|
28
|
+
/** `critic.<name>`: the producer of every layer of the critic's critiques. A critic-scoped token may publish only such layers. */
|
|
29
|
+
export declare function criticProducer(criticName: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Builds the critique a critic produced for an entry. It fills in `schema`, `subject`, `critic`, `model` and
|
|
32
|
+
* `parameters` from the critic and the entry (so they cannot disagree with what was run) and keeps the verdict,
|
|
33
|
+
* findings and usage as given.
|
|
34
|
+
*
|
|
35
|
+
* @param critic the critic that ran.
|
|
36
|
+
* @param description the critic's `describe()` at the time of the run.
|
|
37
|
+
* @param entry the entry and the image hash that was judged.
|
|
38
|
+
* @param answer the critic's answer, or a `failure` instead of one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildCritique(critic: Pick<Critic, 'name' | 'version' | 'kind'>, description: CriticDescription, entry: JudgeableEntry, answer: Pick<Critique, 'verdict' | 'findings' | 'usage' | 'failure'>): Critique;
|
|
41
|
+
/**
|
|
42
|
+
* The checks of one layer for a set of outcomes, in the order given. A verdict is a `passed` check (the critic ran), a
|
|
43
|
+
* failure a `failed` one with its reason code in `detail`. The score is a measurement named `polish`; each finding is
|
|
44
|
+
* also an annotation of type `critique`. Nothing here decides a result: a low score is `passed`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function critiqueChecks(outcomes: readonly CritiqueOutcome[], options?: {
|
|
47
|
+
readonly threshold?: number;
|
|
48
|
+
}): NonNullable<PublishChecksInput['checks']>;
|
|
49
|
+
/**
|
|
50
|
+
* The input of `publishChecks` for one critic's outcomes: producer `critic.<name>`, part kind `critique`. The model and
|
|
51
|
+
* critic versions are labels and not part of the slot, so a newer version replaces an older one's critique of the same
|
|
52
|
+
* entry. Pass `parameters` to move something (such as a model version) into the slot instead, to compare side by side.
|
|
53
|
+
*
|
|
54
|
+
* @param args.critic the critic that ran.
|
|
55
|
+
* @param args.outcomes what it produced, successes and failures.
|
|
56
|
+
* @param args.recordedAtMs when, epoch milliseconds. Nothing here reads a clock, so the same input is the same layer.
|
|
57
|
+
*/
|
|
58
|
+
export declare function critiqueLayer(args: {
|
|
59
|
+
readonly critic: Pick<Critic, 'name' | 'version'>;
|
|
60
|
+
readonly outcomes: readonly CritiqueOutcome[];
|
|
61
|
+
readonly recordedAtMs: number;
|
|
62
|
+
readonly sha?: string | null;
|
|
63
|
+
readonly branch?: string | null;
|
|
64
|
+
readonly runUrl?: string | null;
|
|
65
|
+
readonly environment?: string | null;
|
|
66
|
+
readonly parameters?: Readonly<Record<string, string>>;
|
|
67
|
+
readonly threshold?: number;
|
|
68
|
+
}): PublishChecksInput;
|
|
69
|
+
/**
|
|
70
|
+
* Reads the critique of a check, or `null` when there is none that this reader can read: no `atlas.critique` extension,
|
|
71
|
+
* one that is malformed (a missing `subject`, a score that is not a number), or one of a version this release does not
|
|
72
|
+
* know. The check and its status are never affected: an unreadable opinion is unknown, not a pass and not a failure.
|
|
73
|
+
* Fields and values it does not know (a new category, severity or label) are kept.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readCritique(check: {
|
|
76
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
77
|
+
}): Critique | null;
|
|
78
|
+
/** A critique found in a layer, with where it was found and whether the critic ran. */
|
|
79
|
+
export type FoundCritique = {
|
|
80
|
+
readonly layerId: string;
|
|
81
|
+
readonly producer: string;
|
|
82
|
+
readonly recordedAtMs: number;
|
|
83
|
+
readonly checkId: string;
|
|
84
|
+
/** The check's own status: whether the critic ran, never what it thought. */
|
|
85
|
+
readonly status: string;
|
|
86
|
+
readonly critique: Critique;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Every readable critique in a layer's parts of kind `critique`. Checks whose critique cannot be read are left out, and
|
|
90
|
+
* so is any `atlas.critique` extension in a part of another kind: a test result is never mistaken for an opinion.
|
|
91
|
+
*/
|
|
92
|
+
export declare function critiquesOfLayer(layer: {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly producer: string;
|
|
95
|
+
readonly parts: readonly {
|
|
96
|
+
readonly kind: string;
|
|
97
|
+
readonly recordedAtMs: number;
|
|
98
|
+
readonly checks: readonly {
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly status: string;
|
|
101
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
102
|
+
}[];
|
|
103
|
+
}[];
|
|
104
|
+
}): FoundCritique[];
|
|
105
|
+
/**
|
|
106
|
+
* Whether a critique judged the image an entry has now. `current`: yes. `earlier-image`: the entry's image has changed
|
|
107
|
+
* since, so the critique is shown as judging an earlier image and the entry counts as not yet judged by that critic.
|
|
108
|
+
*/
|
|
109
|
+
export declare function critiqueCurrency(critique: Pick<Critique, 'subject'>, currentImageSha256: string): 'current' | 'earlier-image';
|
|
110
|
+
/**
|
|
111
|
+
* The identity of a critique for caching: the critic, its version, the model, the fixed parameters (the rubric hash
|
|
112
|
+
* among them) and the image hash. Two critiques with the same identity are the same judgement, and the second is not
|
|
113
|
+
* made. Key order in `parameters` does not matter.
|
|
114
|
+
*/
|
|
115
|
+
export declare function critiqueIdentity(args: {
|
|
116
|
+
readonly critic: {
|
|
117
|
+
readonly name: string;
|
|
118
|
+
readonly version: string;
|
|
119
|
+
};
|
|
120
|
+
readonly model?: {
|
|
121
|
+
readonly id: string;
|
|
122
|
+
readonly provider?: string | undefined;
|
|
123
|
+
} | undefined;
|
|
124
|
+
readonly parameters?: Readonly<Record<string, unknown>> | undefined;
|
|
125
|
+
readonly imageSha256: string;
|
|
126
|
+
}): string;
|
|
127
|
+
/** The identity of an existing critique, as {@link critiqueIdentity} computes it for a run. */
|
|
128
|
+
export declare function identityOfCritique(critique: Critique): string;
|