@letta-ai/letta-code 0.31.6 → 0.31.8
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/dist/mcp-client.js +8 -8
- package/dist/mcp-client.js.map +3 -3
- package/dist/memory-constraints.js +18 -0
- package/dist/memory-constraints.js.map +10 -0
- package/dist/types/agent/memory-constraints.d.ts +1 -1
- package/dist/types/agent/memory-constraints.d.ts.map +1 -1
- package/dist/types/agent/subagents/index.d.ts +0 -2
- package/dist/types/agent/subagents/index.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-model.d.ts +20 -0
- package/dist/types/agent/subagents/subagent-model.d.ts.map +1 -1
- package/dist/types/{utils → agent}/system-prompt-size.d.ts +8 -4
- package/dist/types/agent/system-prompt-size.d.ts.map +1 -0
- package/dist/types/backend/backend.d.ts +7 -0
- package/dist/types/backend/backend.d.ts.map +1 -1
- package/dist/types/cli/helpers/system-prompt-warning.d.ts +2 -1
- package/dist/types/cli/helpers/system-prompt-warning.d.ts.map +1 -1
- package/dist/types/mcp-client.d.ts +6 -0
- package/dist/types/mcp-client.d.ts.map +1 -1
- package/dist/types/memory-constraints.d.ts +15 -0
- package/dist/types/memory-constraints.d.ts.map +1 -0
- package/dist/types/permissions/shell-analysis.d.ts.map +1 -1
- package/dist/types/providers/openai-codex-provider.d.ts +2 -10
- package/dist/types/providers/openai-codex-provider.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts +20 -2
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/types/chatgpt-oauth.d.ts +9 -0
- package/dist/types/types/chatgpt-oauth.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +3 -3
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/utils/secrets-store.d.ts +4 -0
- package/dist/types/utils/secrets-store.d.ts.map +1 -1
- package/letta.js +29378 -28407
- package/package.json +11 -1
- package/scripts/agent-watch/verify-pr-identity.test.ts +97 -0
- package/scripts/agent-watch/verify-pr-identity.ts +119 -0
- package/scripts/builtin-skills-watch/aggregate-results.ts +13 -8
- package/scripts/builtin-skills-watch/evidence.test.ts +61 -17
- package/scripts/builtin-skills-watch/evidence.ts +66 -38
- package/scripts/builtin-skills-watch/finalize-result.test.ts +125 -0
- package/scripts/builtin-skills-watch/finalize-result.ts +44 -0
- package/scripts/builtin-skills-watch/reconcile-results.test.ts +58 -0
- package/scripts/builtin-skills-watch/reconcile-results.ts +311 -0
- package/scripts/builtin-skills-watch/result-artifacts.test.ts +83 -0
- package/scripts/builtin-skills-watch/result-artifacts.ts +160 -0
- package/scripts/builtin-skills-watch/update-tracker.test.ts +17 -1
- package/scripts/builtin-skills-watch/update-tracker.ts +89 -15
- package/scripts/claude-watch/update-tracker.test.ts +30 -0
- package/scripts/claude-watch/update-tracker.ts +19 -5
- package/scripts/codex-watch/update-tracker.test.ts +28 -0
- package/scripts/codex-watch/update-tracker.ts +59 -7
- package/scripts/source-file-size-baseline.json +2 -1
- package/skills/browser-use/SKILL.md +363 -0
- package/skills/browser-use/references/recording.md +60 -0
- package/skills/letta-guide/SKILL.md +6 -2
- package/skills/self-configuration/SKILL.md +3 -1
- package/dist/types/utils/system-prompt-size.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.8",
|
|
4
4
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.10",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"dist/app-server-client.cjs.map",
|
|
23
23
|
"dist/memory-confinement.js",
|
|
24
24
|
"dist/memory-confinement.js.map",
|
|
25
|
+
"dist/memory-constraints.js",
|
|
26
|
+
"dist/memory-constraints.js.map",
|
|
25
27
|
"dist/mcp-client.js",
|
|
26
28
|
"dist/mcp-client.js.map",
|
|
27
29
|
"dist/agent-presets.js",
|
|
@@ -58,6 +60,11 @@
|
|
|
58
60
|
"import": "./dist/memory-confinement.js",
|
|
59
61
|
"default": "./dist/memory-confinement.js"
|
|
60
62
|
},
|
|
63
|
+
"./memory-constraints": {
|
|
64
|
+
"types": "./dist/types/memory-constraints.d.ts",
|
|
65
|
+
"import": "./dist/memory-constraints.js",
|
|
66
|
+
"default": "./dist/memory-constraints.js"
|
|
67
|
+
},
|
|
61
68
|
"./mcp-client": {
|
|
62
69
|
"types": "./dist/types/mcp-client.d.ts",
|
|
63
70
|
"import": "./dist/mcp-client.js",
|
|
@@ -211,6 +218,9 @@
|
|
|
211
218
|
"memory-confinement": [
|
|
212
219
|
"./dist/types/memory-confinement.d.ts"
|
|
213
220
|
],
|
|
221
|
+
"memory-constraints": [
|
|
222
|
+
"./dist/types/memory-constraints.d.ts"
|
|
223
|
+
],
|
|
214
224
|
"mcp-client": [
|
|
215
225
|
"./dist/types/mcp-client.d.ts"
|
|
216
226
|
],
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
enforceWatcherPrIdentity,
|
|
4
|
+
parseArgs,
|
|
5
|
+
type WatcherPullRequest,
|
|
6
|
+
} from "./verify-pr-identity.ts";
|
|
7
|
+
|
|
8
|
+
const marker = "Pi-ai-watch: 1.0.0...1.0.1";
|
|
9
|
+
|
|
10
|
+
function pullRequest(
|
|
11
|
+
overrides: Partial<WatcherPullRequest> = {},
|
|
12
|
+
): WatcherPullRequest {
|
|
13
|
+
return {
|
|
14
|
+
author: { login: "amelia-letta" },
|
|
15
|
+
body: marker,
|
|
16
|
+
isDraft: true,
|
|
17
|
+
number: 123,
|
|
18
|
+
url: "https://github.com/letta-ai/letta-code/pull/123",
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function enforce(
|
|
24
|
+
pullRequests: WatcherPullRequest[],
|
|
25
|
+
closed: string[] = [],
|
|
26
|
+
): WatcherPullRequest | null {
|
|
27
|
+
return enforceWatcherPrIdentity({
|
|
28
|
+
repo: "letta-ai/letta-code",
|
|
29
|
+
marker,
|
|
30
|
+
expectedLogin: "amelia-letta",
|
|
31
|
+
pullRequests,
|
|
32
|
+
closePullRequest: (candidate) => closed.push(candidate.url),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("enforceWatcherPrIdentity", () => {
|
|
37
|
+
test("returns the matching Amelia draft PR", () => {
|
|
38
|
+
expect(enforce([pullRequest()])?.number).toBe(123);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("ignores unrelated open PRs", () => {
|
|
42
|
+
expect(enforce([pullRequest({ body: "unrelated" })])).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("closes and rejects a PR from the wrong author", () => {
|
|
46
|
+
const closed: string[] = [];
|
|
47
|
+
expect(() =>
|
|
48
|
+
enforce([pullRequest({ author: { login: "carenthomas" } })], closed),
|
|
49
|
+
).toThrow("author=carenthomas");
|
|
50
|
+
expect(closed).toEqual(["https://github.com/letta-ai/letta-code/pull/123"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("closes and rejects a ready PR", () => {
|
|
54
|
+
const closed: string[] = [];
|
|
55
|
+
expect(() => enforce([pullRequest({ isDraft: false })], closed)).toThrow(
|
|
56
|
+
"draft=false",
|
|
57
|
+
);
|
|
58
|
+
expect(closed).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rejects duplicate valid PRs", () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
enforce([
|
|
64
|
+
pullRequest(),
|
|
65
|
+
pullRequest({
|
|
66
|
+
number: 124,
|
|
67
|
+
url: "https://github.com/letta-ai/letta-code/pull/124",
|
|
68
|
+
}),
|
|
69
|
+
]),
|
|
70
|
+
).toThrow("Multiple open watcher PRs");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("parseArgs", () => {
|
|
75
|
+
test("requires every identity input", () => {
|
|
76
|
+
expect(() => parseArgs(["--repo", "letta-ai/letta-code"])).toThrow(
|
|
77
|
+
"--marker is required",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("parses a complete invocation", () => {
|
|
82
|
+
expect(
|
|
83
|
+
parseArgs([
|
|
84
|
+
"--repo",
|
|
85
|
+
"letta-ai/letta-code",
|
|
86
|
+
"--marker",
|
|
87
|
+
marker,
|
|
88
|
+
"--expected-login",
|
|
89
|
+
"amelia-letta",
|
|
90
|
+
]),
|
|
91
|
+
).toEqual({
|
|
92
|
+
repo: "letta-ai/letta-code",
|
|
93
|
+
marker,
|
|
94
|
+
expectedLogin: "amelia-letta",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export interface WatcherPullRequest {
|
|
6
|
+
author: { login: string } | null;
|
|
7
|
+
body: string | null;
|
|
8
|
+
isDraft: boolean;
|
|
9
|
+
number: number;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Args {
|
|
14
|
+
expectedLogin: string;
|
|
15
|
+
marker: string;
|
|
16
|
+
repo: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface EnforceOptions extends Args {
|
|
20
|
+
closePullRequest: (pullRequest: WatcherPullRequest) => void;
|
|
21
|
+
pullRequests: WatcherPullRequest[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function enforceWatcherPrIdentity(
|
|
25
|
+
options: EnforceOptions,
|
|
26
|
+
): WatcherPullRequest | null {
|
|
27
|
+
const matches = options.pullRequests.filter((pullRequest) =>
|
|
28
|
+
pullRequest.body?.includes(options.marker),
|
|
29
|
+
);
|
|
30
|
+
const invalid = matches.filter(
|
|
31
|
+
(pullRequest) =>
|
|
32
|
+
pullRequest.author?.login !== options.expectedLogin ||
|
|
33
|
+
!pullRequest.isDraft,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
for (const pullRequest of invalid) options.closePullRequest(pullRequest);
|
|
37
|
+
|
|
38
|
+
if (invalid.length > 0) {
|
|
39
|
+
const details = invalid
|
|
40
|
+
.map(
|
|
41
|
+
(pullRequest) =>
|
|
42
|
+
`${pullRequest.url} (author=${pullRequest.author?.login ?? "unknown"}, draft=${pullRequest.isDraft})`,
|
|
43
|
+
)
|
|
44
|
+
.join(", ");
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Closed watcher PRs that did not match author=${options.expectedLogin} and draft=true: ${details}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (matches.length > 1) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Multiple open watcher PRs contain marker ${JSON.stringify(options.marker)}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return matches[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseArgs(argv: string[]): Args {
|
|
58
|
+
let repo = "";
|
|
59
|
+
let marker = "";
|
|
60
|
+
let expectedLogin = "";
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const argument = argv[index];
|
|
63
|
+
if (argument === "--repo") repo = argv[++index] ?? "";
|
|
64
|
+
else if (argument === "--marker") marker = argv[++index] ?? "";
|
|
65
|
+
else if (argument === "--expected-login") {
|
|
66
|
+
expectedLogin = argv[++index] ?? "";
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!/^[^/]+\/[^/]+$/.test(repo)) throw new Error("--repo is required");
|
|
72
|
+
if (!marker) throw new Error("--marker is required");
|
|
73
|
+
if (!expectedLogin) throw new Error("--expected-login is required");
|
|
74
|
+
return { repo, marker, expectedLogin };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runGh(args: string[]): string {
|
|
78
|
+
const result = spawnSync("gh", args, {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
});
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
throw new Error(`gh ${args.join(" ")} failed:\n${result.stderr}`);
|
|
85
|
+
}
|
|
86
|
+
return result.stdout;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function main(): void {
|
|
90
|
+
const args = parseArgs(process.argv.slice(2));
|
|
91
|
+
const pullRequests = JSON.parse(
|
|
92
|
+
runGh([
|
|
93
|
+
"pr",
|
|
94
|
+
"list",
|
|
95
|
+
"--repo",
|
|
96
|
+
args.repo,
|
|
97
|
+
"--state",
|
|
98
|
+
"open",
|
|
99
|
+
"--limit",
|
|
100
|
+
"1000",
|
|
101
|
+
"--json",
|
|
102
|
+
"author,body,isDraft,number,url",
|
|
103
|
+
]),
|
|
104
|
+
) as WatcherPullRequest[];
|
|
105
|
+
const verified = enforceWatcherPrIdentity({
|
|
106
|
+
...args,
|
|
107
|
+
pullRequests,
|
|
108
|
+
closePullRequest: (pullRequest) => {
|
|
109
|
+
runGh(["pr", "close", pullRequest.url, "--repo", args.repo]);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
console.log(
|
|
113
|
+
verified
|
|
114
|
+
? `Verified Amelia draft PR: ${verified.url}`
|
|
115
|
+
: `No open PR contains marker ${JSON.stringify(args.marker)}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (import.meta.main) main();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/** Validates a daily skill-review batch and updates the tracker once. */
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
type BuiltinSkillWatchAnalysis,
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
DEFAULT_TARGET_REPO,
|
|
10
10
|
} from "./analysis.ts";
|
|
11
11
|
import { createIssueComment, editIssueBody } from "./github.ts";
|
|
12
|
+
import {
|
|
13
|
+
discoverReviewArtifacts,
|
|
14
|
+
formatFailureReceipt,
|
|
15
|
+
} from "./result-artifacts.ts";
|
|
12
16
|
import {
|
|
13
17
|
hasTerminalCandidate,
|
|
14
18
|
parseTrackerState,
|
|
@@ -103,6 +107,7 @@ function main(): void {
|
|
|
103
107
|
let state = parseTrackerState(original.body);
|
|
104
108
|
let failed = false;
|
|
105
109
|
const outcomes: ValidatedOutcome[] = [];
|
|
110
|
+
const artifacts = discoverReviewArtifacts(args.resultsDir as string);
|
|
106
111
|
|
|
107
112
|
for (const candidate of manifest.candidates) {
|
|
108
113
|
const analysisPath = join(
|
|
@@ -130,14 +135,14 @@ function main(): void {
|
|
|
130
135
|
analysis.workflow_run_url = workflowRunUrl(pending.workflow_run_id);
|
|
131
136
|
assertAnalysisIdentity(received, analysis);
|
|
132
137
|
|
|
133
|
-
const resultPath =
|
|
134
|
-
args.resultsDir as string,
|
|
135
|
-
`builtin-skill-result-${received.skill}`,
|
|
136
|
-
"result.json",
|
|
137
|
-
);
|
|
138
|
+
const resultPath = artifacts.results.get(received.candidate_id);
|
|
138
139
|
try {
|
|
139
|
-
if (!
|
|
140
|
-
|
|
140
|
+
if (!resultPath) {
|
|
141
|
+
const receipt = artifacts.failures.get(received.candidate_id);
|
|
142
|
+
if (receipt) throw new Error(formatFailureReceipt(receipt));
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Amelia result artifact is missing for ${received.candidate_id}`,
|
|
145
|
+
);
|
|
141
146
|
}
|
|
142
147
|
const result = readReviewResult(resultPath, analysis);
|
|
143
148
|
if (result.pr_url) {
|
|
@@ -29,6 +29,27 @@ describe("review evidence", () => {
|
|
|
29
29
|
expect(digestReviewEvidence(evidence)).toMatch(/^[a-f0-9]{64}$/);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test("accepts RFC 3339 UTC timestamps without milliseconds", () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
parseReviewEvidence({
|
|
35
|
+
schema_version: 1,
|
|
36
|
+
candidate_id: "candidate",
|
|
37
|
+
skill: "creating-skills",
|
|
38
|
+
sources: [
|
|
39
|
+
{
|
|
40
|
+
locator: "src/skills/builtin/creating-skills/SKILL.md",
|
|
41
|
+
revision: "a".repeat(40),
|
|
42
|
+
content_digest: null,
|
|
43
|
+
retrieved_at: "2026-08-26T00:00:00Z",
|
|
44
|
+
excerpt: "current source",
|
|
45
|
+
claims: ["checked current source"],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
probes: [],
|
|
49
|
+
}),
|
|
50
|
+
).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
32
53
|
test("requires at least one source", () => {
|
|
33
54
|
expect(() =>
|
|
34
55
|
parseReviewEvidence({
|
|
@@ -38,7 +59,7 @@ describe("review evidence", () => {
|
|
|
38
59
|
sources: [],
|
|
39
60
|
probes: [],
|
|
40
61
|
}),
|
|
41
|
-
).toThrow("
|
|
62
|
+
).toThrow("between 1 and 5 sources");
|
|
42
63
|
});
|
|
43
64
|
|
|
44
65
|
test("rejects malformed digests and timestamps", () => {
|
|
@@ -59,28 +80,51 @@ describe("review evidence", () => {
|
|
|
59
80
|
],
|
|
60
81
|
probes: [],
|
|
61
82
|
}),
|
|
62
|
-
).toThrow("
|
|
83
|
+
).toThrow("content_digest is invalid");
|
|
63
84
|
});
|
|
64
85
|
|
|
65
|
-
test("
|
|
86
|
+
test("accepts production-sized evidence larger than the old tracker limit", () => {
|
|
87
|
+
const evidence = parseReviewEvidence({
|
|
88
|
+
schema_version: 1,
|
|
89
|
+
candidate_id: "candidate",
|
|
90
|
+
skill: "creating-skills",
|
|
91
|
+
sources: Array.from({ length: 5 }, (_, index) => ({
|
|
92
|
+
locator: `https://docs.letta.com/source-${index}`,
|
|
93
|
+
revision: "a".repeat(40),
|
|
94
|
+
content_digest: "b".repeat(64),
|
|
95
|
+
retrieved_at: "2026-08-26T00:00:00.000Z",
|
|
96
|
+
excerpt: "q".repeat(300),
|
|
97
|
+
claims: Array.from({ length: 5 }, () => "z".repeat(300)),
|
|
98
|
+
})),
|
|
99
|
+
probes: [],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(Buffer.byteLength(JSON.stringify(evidence), "utf8")).toBeGreaterThan(
|
|
103
|
+
650,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("still rejects evidence larger than an issue comment can safely hold", () => {
|
|
66
108
|
expect(() =>
|
|
67
109
|
parseReviewEvidence({
|
|
68
110
|
schema_version: 1,
|
|
69
111
|
candidate_id: "candidate",
|
|
70
112
|
skill: "creating-skills",
|
|
71
|
-
sources:
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
sources: Array.from({ length: 5 }, (_, index) => ({
|
|
114
|
+
locator: `https://docs.letta.com/${"x".repeat(450)}-${index}`,
|
|
115
|
+
revision: "y".repeat(300),
|
|
116
|
+
content_digest: null,
|
|
117
|
+
retrieved_at: "2026-08-26T00:00:00.000Z",
|
|
118
|
+
excerpt: "q".repeat(300),
|
|
119
|
+
claims: Array.from({ length: 5 }, () => "z".repeat(500)),
|
|
120
|
+
})),
|
|
121
|
+
probes: Array.from({ length: 5 }, () => ({
|
|
122
|
+
command: "c".repeat(500),
|
|
123
|
+
result_digest: "d".repeat(64),
|
|
124
|
+
summary: "s".repeat(500),
|
|
125
|
+
})),
|
|
82
126
|
}),
|
|
83
|
-
).toThrow("exceeds
|
|
127
|
+
).toThrow("exceeds 16384 bytes");
|
|
84
128
|
});
|
|
85
129
|
|
|
86
130
|
test("requires a revision or digest and rejects unknown fields", () => {
|
|
@@ -100,7 +144,7 @@ describe("review evidence", () => {
|
|
|
100
144
|
sources: [source],
|
|
101
145
|
probes: [],
|
|
102
146
|
}),
|
|
103
|
-
).toThrow("
|
|
147
|
+
).toThrow("requires a revision or content_digest");
|
|
104
148
|
expect(() =>
|
|
105
149
|
parseReviewEvidence({
|
|
106
150
|
schema_version: 1,
|
|
@@ -109,6 +153,6 @@ describe("review evidence", () => {
|
|
|
109
153
|
sources: [{ ...source, content_digest: "a".repeat(64), secret: "no" }],
|
|
110
154
|
probes: [],
|
|
111
155
|
}),
|
|
112
|
-
).toThrow("
|
|
156
|
+
).toThrow("unknown or missing fields");
|
|
113
157
|
});
|
|
114
158
|
});
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
const MAX_SOURCES = 5;
|
|
4
4
|
const MAX_PROBES = 5;
|
|
5
5
|
const MAX_CLAIMS_PER_SOURCE = 5;
|
|
6
|
-
const MAX_SERIALIZED_BYTES =
|
|
6
|
+
const MAX_SERIALIZED_BYTES = 16 * 1024;
|
|
7
7
|
|
|
8
8
|
export interface EvidenceSource {
|
|
9
9
|
locator: string;
|
|
@@ -51,29 +51,24 @@ export function parseReviewEvidence(value: unknown): ReviewEvidence {
|
|
|
51
51
|
if (
|
|
52
52
|
!Array.isArray(value.sources) ||
|
|
53
53
|
value.sources.length === 0 ||
|
|
54
|
-
value.sources.length > MAX_SOURCES
|
|
55
|
-
!value.sources.every(isEvidenceSource)
|
|
54
|
+
value.sources.length > MAX_SOURCES
|
|
56
55
|
) {
|
|
57
|
-
throw new TypeError("review evidence
|
|
56
|
+
throw new TypeError("review evidence must include between 1 and 5 sources");
|
|
58
57
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
value.probes.length > MAX_PROBES ||
|
|
62
|
-
!value.probes.every(isEvidenceProbe)
|
|
63
|
-
) {
|
|
64
|
-
throw new TypeError("review evidence probes are invalid");
|
|
58
|
+
if (!Array.isArray(value.probes) || value.probes.length > MAX_PROBES) {
|
|
59
|
+
throw new TypeError("review evidence must include at most 5 probes");
|
|
65
60
|
}
|
|
66
61
|
const evidence: ReviewEvidence = {
|
|
67
62
|
schema_version: 1,
|
|
68
63
|
candidate_id: value.candidate_id,
|
|
69
64
|
skill: value.skill,
|
|
70
|
-
sources: value.sources,
|
|
71
|
-
probes: value.probes,
|
|
65
|
+
sources: value.sources.map(parseEvidenceSource),
|
|
66
|
+
probes: value.probes.map(parseEvidenceProbe),
|
|
72
67
|
};
|
|
73
68
|
if (
|
|
74
69
|
Buffer.byteLength(JSON.stringify(evidence), "utf8") > MAX_SERIALIZED_BYTES
|
|
75
70
|
) {
|
|
76
|
-
throw new TypeError("review evidence exceeds
|
|
71
|
+
throw new TypeError("review evidence exceeds 16384 bytes");
|
|
77
72
|
}
|
|
78
73
|
return evidence;
|
|
79
74
|
}
|
|
@@ -82,44 +77,77 @@ export function digestReviewEvidence(evidence: ReviewEvidence): string {
|
|
|
82
77
|
return createHash("sha256").update(JSON.stringify(evidence)).digest("hex");
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
function parseEvidenceSource(value: unknown, index: number): EvidenceSource {
|
|
81
|
+
const description = `review evidence source ${index + 1}`;
|
|
82
|
+
if (
|
|
83
|
+
!isRecord(value) ||
|
|
84
|
+
!hasExactKeys(value, [
|
|
89
85
|
"locator",
|
|
90
86
|
"revision",
|
|
91
87
|
"content_digest",
|
|
92
88
|
"retrieved_at",
|
|
93
89
|
"excerpt",
|
|
94
90
|
"claims",
|
|
95
|
-
])
|
|
96
|
-
|
|
97
|
-
(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
])
|
|
92
|
+
) {
|
|
93
|
+
throw new TypeError(`${description} has unknown or missing fields`);
|
|
94
|
+
}
|
|
95
|
+
if (!isBoundedString(value.locator, 500)) {
|
|
96
|
+
throw new TypeError(`${description} locator is invalid`);
|
|
97
|
+
}
|
|
98
|
+
if (value.revision !== null && !isBoundedString(value.revision, 300)) {
|
|
99
|
+
throw new TypeError(`${description} revision is invalid`);
|
|
100
|
+
}
|
|
101
|
+
if (value.content_digest !== null && !isDigest(value.content_digest)) {
|
|
102
|
+
throw new TypeError(`${description} content_digest is invalid`);
|
|
103
|
+
}
|
|
104
|
+
if (value.revision === null && value.content_digest === null) {
|
|
105
|
+
throw new TypeError(`${description} requires a revision or content_digest`);
|
|
106
|
+
}
|
|
107
|
+
if (!isIsoTimestamp(value.retrieved_at)) {
|
|
108
|
+
throw new TypeError(`${description} retrieved_at is not an ISO timestamp`);
|
|
109
|
+
}
|
|
110
|
+
if (!isBoundedString(value.excerpt, 300)) {
|
|
111
|
+
throw new TypeError(`${description} excerpt is invalid`);
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
!Array.isArray(value.claims) ||
|
|
115
|
+
value.claims.length === 0 ||
|
|
116
|
+
value.claims.length > MAX_CLAIMS_PER_SOURCE ||
|
|
117
|
+
!value.claims.every((claim) => isBoundedString(claim, 500))
|
|
118
|
+
) {
|
|
119
|
+
throw new TypeError(`${description} claims are invalid`);
|
|
120
|
+
}
|
|
121
|
+
return value as unknown as EvidenceSource;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
function parseEvidenceProbe(value: unknown, index: number): EvidenceProbe {
|
|
125
|
+
const description = `review evidence probe ${index + 1}`;
|
|
126
|
+
if (
|
|
127
|
+
!isRecord(value) ||
|
|
128
|
+
!hasExactKeys(value, ["command", "result_digest", "summary"])
|
|
129
|
+
) {
|
|
130
|
+
throw new TypeError(`${description} has unknown or missing fields`);
|
|
131
|
+
}
|
|
132
|
+
if (!isBoundedString(value.command, 500)) {
|
|
133
|
+
throw new TypeError(`${description} command is invalid`);
|
|
134
|
+
}
|
|
135
|
+
if (!isDigest(value.result_digest)) {
|
|
136
|
+
throw new TypeError(`${description} result_digest is invalid`);
|
|
137
|
+
}
|
|
138
|
+
if (!isBoundedString(value.summary, 500)) {
|
|
139
|
+
throw new TypeError(`${description} summary is invalid`);
|
|
140
|
+
}
|
|
141
|
+
return value as unknown as EvidenceProbe;
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
function isIsoTimestamp(value: unknown): value is string {
|
|
120
145
|
if (typeof value !== "string") return false;
|
|
121
146
|
const parsed = new Date(value);
|
|
122
|
-
return
|
|
147
|
+
return (
|
|
148
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
149
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)
|
|
150
|
+
);
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
function isDigest(value: unknown): value is string {
|