@davidvornholt/standards 0.14.0 → 0.15.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.
@@ -0,0 +1,117 @@
1
+ // Cloudflare R2 ground truth for the credential broker, pinned by
2
+ // creds-r2.test.ts against https://developers.cloudflare.com/r2/api/tokens/:
3
+ // a bucket-scoped token targets the resource
4
+ // `com.cloudflare.edge.r2.bucket.<account_id>_<jurisdiction>_<bucket>`, and
5
+ // the S3-compatible credential pair derived from a minted token is access
6
+ // key ID = the token's ID, secret access key = the SHA-256 hex digest of the
7
+ // token's value.
8
+
9
+ import { createHash } from 'node:crypto';
10
+ import type { SopsValueChange } from './creds-sops';
11
+
12
+ // Scope the permission_groups endpoint reports for the bucket-item groups
13
+ // ("Workers R2 Storage Bucket Item Read"/"... Write"); account-scoped groups
14
+ // report `com.cloudflare.api.account` instead.
15
+ export const R2_BUCKET_SCOPE = 'com.cloudflare.edge.r2.bucket';
16
+
17
+ export type R2Jurisdiction = 'default' | 'eu';
18
+ export const DEFAULT_R2_JURISDICTION: R2Jurisdiction = 'default';
19
+
20
+ export const isR2Jurisdiction = (value: string): value is R2Jurisdiction =>
21
+ value === DEFAULT_R2_JURISDICTION || value === 'eu';
22
+
23
+ // R2 bucket names are 3-63 characters of lowercase letters, digits, and
24
+ // hyphens, starting and ending alphanumeric.
25
+ const BUCKET_NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/u;
26
+
27
+ export const isR2BucketName = (name: string): boolean => BUCKET_NAME.test(name);
28
+
29
+ export const r2BucketResource = (
30
+ accountId: string,
31
+ bucket: string,
32
+ jurisdiction: R2Jurisdiction = DEFAULT_R2_JURISDICTION,
33
+ ): string => `${R2_BUCKET_SCOPE}.${accountId}_${jurisdiction}_${bucket}`;
34
+
35
+ export const deriveS3SecretAccessKey = (tokenValue: string): string =>
36
+ createHash('sha256').update(tokenValue).digest('hex');
37
+
38
+ export const s3Endpoint = (
39
+ accountId: string,
40
+ jurisdiction: R2Jurisdiction = DEFAULT_R2_JURISDICTION,
41
+ ): string =>
42
+ `https://${accountId}.${jurisdiction === 'eu' ? 'eu.' : ''}r2.cloudflarestorage.com`;
43
+
44
+ export type DestinationFormat = 'bearer' | 's3';
45
+
46
+ const ACCESS_KEY_SEGMENT = 'access_key_id';
47
+ const SECRET_KEY_SEGMENT = 'secret_access_key';
48
+ const S3_PAIR_SEGMENTS = [ACCESS_KEY_SEGMENT, SECRET_KEY_SEGMENT] as const;
49
+
50
+ export const s3AccessKeyPath = (key: string): string =>
51
+ `${key}.${ACCESS_KEY_SEGMENT}`;
52
+
53
+ export const s3PairPaths = (key: string): ReadonlyArray<string> => [
54
+ s3AccessKeyPath(key),
55
+ `${key}.${SECRET_KEY_SEGMENT}`,
56
+ ];
57
+
58
+ export const destinationFootprint = (
59
+ format: DestinationFormat,
60
+ key: string,
61
+ ): ReadonlyArray<string> => (format === 's3' ? s3PairPaths(key) : [key]);
62
+
63
+ export const destinationFootprintsIntersect = (
64
+ left: ReadonlyArray<string>,
65
+ right: ReadonlyArray<string>,
66
+ ): boolean => left.some((path) => right.includes(path));
67
+
68
+ // The SOPS writes for one minted token: a bearer destination stores the raw
69
+ // token value at the key itself; an S3 destination stores the derived
70
+ // credential pair under it.
71
+ export const destinationWrites = (
72
+ format: DestinationFormat,
73
+ key: string,
74
+ tokenId: string,
75
+ tokenValue: string,
76
+ ): ReadonlyArray<SopsValueChange> =>
77
+ format === 'bearer'
78
+ ? [{ path: key, value: tokenValue }]
79
+ : [
80
+ { path: `${key}.${ACCESS_KEY_SEGMENT}`, value: tokenId },
81
+ {
82
+ path: `${key}.${SECRET_KEY_SEGMENT}`,
83
+ value: deriveS3SecretAccessKey(tokenValue),
84
+ },
85
+ ];
86
+
87
+ // Reconciliation infers a destination's format from the plaintext SOPS key
88
+ // structure: a leaf at the key is a bearer token, both pair leaves under it
89
+ // are an S3 credential, and one pair leaf without the other is unsafe to
90
+ // mutate automatically.
91
+ export const destinationFormatOf = (
92
+ keys: ReadonlySet<string> | undefined,
93
+ key: string,
94
+ ): DestinationFormat | 'partial' | 'absent' => {
95
+ if (keys === undefined) {
96
+ return 'absent';
97
+ }
98
+ if (keys.has(key)) {
99
+ return 'bearer';
100
+ }
101
+ const present = s3PairPaths(key).filter((path) => keys.has(path));
102
+ if (present.length === S3_PAIR_SEGMENTS.length) {
103
+ return 's3';
104
+ }
105
+ return present.length === 0 ? 'absent' : 'partial';
106
+ };
107
+
108
+ export const inferredDestinationFootprint = (
109
+ keys: ReadonlySet<string> | undefined,
110
+ key: string,
111
+ ): ReadonlyArray<string> => {
112
+ const format = destinationFormatOf(keys, key);
113
+ return destinationFootprint(
114
+ format === 's3' || format === 'partial' ? 's3' : 'bearer',
115
+ key,
116
+ );
117
+ };
package/src/creds-sops.ts CHANGED
@@ -189,10 +189,3 @@ export const setSopsValues = (
189
189
  problem: `could not write ${paths} into ${rel}${detail ? `: ${detail}` : ''}`,
190
190
  };
191
191
  };
192
-
193
- export const setSopsValue = (
194
- consumer: string,
195
- rel: string,
196
- path: string,
197
- value: string,
198
- ): SopsWriteResult => setSopsValues(consumer, rel, [{ path, value }]);
@@ -34,6 +34,13 @@ type CodexExecutor = (
34
34
 
35
35
  const defaultExecutor: CodexExecutor = execFileSync;
36
36
 
37
+ type CodexRunOptions = {
38
+ readonly workDir: string;
39
+ readonly gitCommonDir: string;
40
+ readonly prompt: string;
41
+ readonly config: PollerConfig;
42
+ };
43
+
37
44
  const exitCause = (details: Record<string, unknown>): string => {
38
45
  if (typeof details.status === 'number') {
39
46
  return `exit status ${details.status}`;
@@ -57,11 +64,10 @@ const agentEnv = (): Record<string, string | undefined> => {
57
64
  };
58
65
 
59
66
  export const runCodex = (
60
- workDir: string,
61
- prompt: string,
62
- config: PollerConfig,
67
+ options: CodexRunOptions,
63
68
  execute: CodexExecutor = defaultExecutor,
64
69
  ): CodexRunResult => {
70
+ const { workDir, gitCommonDir, prompt, config } = options;
65
71
  rmSync(join(workDir, OUTCOME_DIR), { recursive: true, force: true });
66
72
  try {
67
73
  execute(
@@ -72,6 +78,8 @@ export const runCodex = (
72
78
  workDir,
73
79
  '--sandbox',
74
80
  'workspace-write',
81
+ '--add-dir',
82
+ gitCommonDir,
75
83
  '-c',
76
84
  'sandbox_workspace_write.network_access=true',
77
85
  '-m',
@@ -139,9 +139,10 @@ export const runFixJob = async (
139
139
  workspace,
140
140
  };
141
141
  try {
142
- const run = runCodex(
143
- workspace.dir,
144
- fixPrompt({
142
+ const run = runCodex({
143
+ workDir: workspace.dir,
144
+ gitCommonDir: cacheClone,
145
+ prompt: fixPrompt({
145
146
  repo,
146
147
  issueNumber: issue.number,
147
148
  title: currentIssue.title,
@@ -149,7 +150,7 @@ export const runFixJob = async (
149
150
  answers: preamble.answers,
150
151
  }),
151
152
  config,
152
- );
153
+ });
153
154
  await validateFixClaim(job);
154
155
  const outcome = run.succeeded ? await readFixOutcome(workspace.dir) : null;
155
156
  if (outcome === null) {
@@ -36,17 +36,18 @@ export const executeReviewJob = async (options: {
36
36
  ),
37
37
  );
38
38
  try {
39
- const run = runCodex(
40
- workspace.dir,
41
- reviewPrompt({
39
+ const run = runCodex({
40
+ workDir: workspace.dir,
41
+ gitCommonDir: cacheClone,
42
+ prompt: reviewPrompt({
42
43
  repo: deps.repo,
43
44
  prNumber: pr.number,
44
45
  title: pr.title,
45
46
  baseSha: reviewBase,
46
47
  answers,
47
48
  }),
48
- deps.config,
49
- );
49
+ config: deps.config,
50
+ });
50
51
  await validateReviewClaim({
51
52
  deps,
52
53
  pr,