@popcorn245/cornfield 0.9.0 → 0.11.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/core/agent-contract.js +21 -4
- package/core/agent-contract.schema.json +40 -9
- package/core/config.js +40 -8
- package/explorer/forge-panel.js +7 -1
- package/explorer/index.html +2 -1
- package/explorer/review-tokens-model.js +10 -3
- package/explorer/review-tokens.js +4 -2
- package/explorer/runs-model.js +18 -0
- package/package.json +1 -1
- package/server/agent-api.js +4 -3
- package/server/agent-runs.js +214 -43
- package/server/auth.js +58 -3
- package/server/db.js +31 -0
- package/server/forge-actions.js +1 -1
- package/server/hooks.js +37 -3
- package/server/review-api.js +10 -8
- package/server/review-tokens.js +24 -7
- package/server/runner-api.js +270 -0
- package/server/server.js +26 -4
- package/server/spec-poll.js +26 -16
- package/server/threads.js +4 -2
- package/server/triggers.js +284 -0
package/core/agent-contract.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @typedef {{ issue?: number, proposal?: string, pull?: number, thread?: number }} RunRefs
|
|
40
40
|
* @typedef {{ minutes?: number, turns?: number, cost?: number }} RunLimits
|
|
41
41
|
* @typedef {{ version: 1, runId: string, kind: RunKind, profile?: string, refs: RunRefs, branch?: string, brief: string,
|
|
42
|
-
* limits?: RunLimits, nodes?: string[], requestedBy: string, requestedAt: string }} RunRequest
|
|
42
|
+
* limits?: RunLimits, nodes?: string[], requestedBy: string, requestedAt: string, trigger?: { event: 'spec.approved' | 'build.opened' | 'thread.message', by: string, ref: string } }} RunRequest
|
|
43
43
|
* @typedef {'claimed' | 'working' | 'done' | 'failed' | 'stopped'} ReportState
|
|
44
44
|
* @typedef {{ state: ReportState, note?: string, url?: string, tokens?: number, cost?: number }} RunReport
|
|
45
45
|
* @typedef {{ state: 'stop-requested', note?: string }} StopRequest
|
|
@@ -83,7 +83,10 @@ export const RUN_TAG = 'cornfield-run';
|
|
|
83
83
|
/** The key of the trailer */
|
|
84
84
|
export const TRAILER_KEY = 'Cornfield-Run';
|
|
85
85
|
|
|
86
|
-
const REQUEST_KEYS = ['version', 'runId', 'kind', 'profile', 'refs', 'branch', 'brief', 'limits', 'nodes', 'requestedBy', 'requestedAt'];
|
|
86
|
+
const REQUEST_KEYS = ['version', 'runId', 'kind', 'profile', 'refs', 'branch', 'brief', 'limits', 'nodes', 'requestedBy', 'requestedAt', 'trigger'];
|
|
87
|
+
const TRIGGER_KEYS = ['event', 'by', 'ref'];
|
|
88
|
+
/** The events an opt-in trigger may fire on (`agents.on`, spec 0005 D10): each is something a maintainer did */
|
|
89
|
+
export const TRIGGER_EVENTS = Object.freeze(['spec.approved', 'build.opened', 'thread.message']);
|
|
87
90
|
const REF_KEYS = ['issue', 'proposal', 'pull', 'thread'];
|
|
88
91
|
const LIMIT_KEYS = ['minutes', 'turns', 'cost'];
|
|
89
92
|
const REPORT_KEYS = ['state', 'note', 'url', 'tokens', 'cost'];
|
|
@@ -101,6 +104,8 @@ export const LOGIN_SOURCE = '^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$';
|
|
|
101
104
|
export const SLUG_SOURCE = `^[A-Za-z0-9][A-Za-z0-9._-]{0,${MAX_SLUG_CHARS - 1}}$`;
|
|
102
105
|
export const PROFILE_SOURCE = `^[A-Za-z0-9][A-Za-z0-9._-]{0,${MAX_PROFILE_CHARS - 1}}$`;
|
|
103
106
|
export const NODE_ID_SOURCE = `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,${MAX_NODE_CHARS - 1}}$`;
|
|
107
|
+
/** What an automatic request says caused it: a pull request, an issue or a comment, by its number or id */
|
|
108
|
+
export const TRIGGER_REF_SOURCE = '^(pull|issue|comment):[0-9]{1,20}$';
|
|
104
109
|
export const ISO_SOURCE = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$';
|
|
105
110
|
/** An http(s) link with no user and password in it */
|
|
106
111
|
export const URL_SOURCE = '^https?://[^\\s/@?#]+([/?#]\\S*)?$';
|
|
@@ -118,7 +123,7 @@ export const CREDENTIAL_KEY_SOURCE = `(?:${['token', 'secret', 'passw', 'credent
|
|
|
118
123
|
const EDGE = '(?<![A-Za-z0-9])';
|
|
119
124
|
|
|
120
125
|
/**
|
|
121
|
-
* A value shaped like a credential: a forge or cloud token, cornfield's own API tokens (`cf_rt_…` review, `cf_at_…` agent), a JWT, a
|
|
126
|
+
* A value shaped like a credential: a forge or cloud token, cornfield's own API tokens (`cf_rt_…` review, `cf_at_…` agent, `cf_nt_…` runner), a JWT, a
|
|
122
127
|
* private key, a bearer header, a user and password in a link.
|
|
123
128
|
* Prose that only talks about tokens and passwords passes; `sk-` keys and bearer values need a digit in their first 41
|
|
124
129
|
* characters, as real ones have, so a long hyphenated word after "Bearer" or "sk-" is not taken for one. Each alternative tries a
|
|
@@ -129,7 +134,7 @@ export const CREDENTIAL_VALUE_SOURCE = [
|
|
|
129
134
|
`${EDGE}github_pat_[A-Za-z0-9_]{20,}`,
|
|
130
135
|
`${EDGE}glpat-[A-Za-z0-9_-]{15,}`,
|
|
131
136
|
`${EDGE}gto-[A-Za-z0-9]{20,}`,
|
|
132
|
-
`${EDGE}cf_(?:rt|at)_[A-Za-z0-9_-]{20,}`,
|
|
137
|
+
`${EDGE}cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}`,
|
|
133
138
|
`${EDGE}sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]`,
|
|
134
139
|
`${EDGE}AKIA[0-9A-Z]{16}`,
|
|
135
140
|
`${EDGE}AIza[0-9A-Za-z_-]{35}`,
|
|
@@ -150,6 +155,7 @@ const SLUG = new RegExp(SLUG_SOURCE);
|
|
|
150
155
|
const PROFILE = new RegExp(PROFILE_SOURCE);
|
|
151
156
|
const NODE_ID = new RegExp(NODE_ID_SOURCE);
|
|
152
157
|
const ISO = new RegExp(ISO_SOURCE);
|
|
158
|
+
const TRIGGER_REF = new RegExp(TRIGGER_REF_SOURCE);
|
|
153
159
|
const URL_RE = new RegExp(URL_SOURCE);
|
|
154
160
|
const CREDENTIAL_KEY = new RegExp(CREDENTIAL_KEY_SOURCE);
|
|
155
161
|
const CREDENTIAL_VALUE = new RegExp(CREDENTIAL_VALUE_SOURCE);
|
|
@@ -355,6 +361,17 @@ export function validateRequest(body, { profiles } = {}) {
|
|
|
355
361
|
if (body.nodes !== undefined) value.nodes = [...body.nodes];
|
|
356
362
|
value.requestedBy = body.requestedBy;
|
|
357
363
|
value.requestedAt = body.requestedAt;
|
|
364
|
+
if (body.trigger !== undefined) {
|
|
365
|
+
if (!isObject(body.trigger)) return no('"trigger" is { event, by, ref }: what caused an automatic request.');
|
|
366
|
+
const triggerKeys = keyProblem(body.trigger, TRIGGER_KEYS, '"trigger"');
|
|
367
|
+
if (triggerKeys) return no(triggerKeys);
|
|
368
|
+
if (!TRIGGER_EVENTS.includes(body.trigger.event)) return no(`"trigger.event" is one of ${TRIGGER_EVENTS.join(', ')}.`);
|
|
369
|
+
if (typeof body.trigger.by !== 'string' || !LOGIN.test(body.trigger.by)) return no('"trigger.by" is the forge login of the maintainer whose act caused the request.');
|
|
370
|
+
if (typeof body.trigger.ref !== 'string' || !TRIGGER_REF.test(body.trigger.ref)) return no('"trigger.ref" is what the act was on: pull:<number>, issue:<number> or comment:<id>.');
|
|
371
|
+
// The request is posted as the maintainer whose act caused it, so the two name one person
|
|
372
|
+
if (body.trigger.by !== body.requestedBy) return no('"trigger.by" is the maintainer the request is made as, the same login as "requestedBy".');
|
|
373
|
+
value.trigger = { event: body.trigger.event, by: body.trigger.by, ref: body.trigger.ref };
|
|
374
|
+
}
|
|
358
375
|
return { ok: true, value };
|
|
359
376
|
}
|
|
360
377
|
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"type": "string",
|
|
58
58
|
"maxLength": 64,
|
|
59
59
|
"not": {
|
|
60
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
60
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
61
61
|
},
|
|
62
62
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
|
|
63
63
|
"description": "A name the runner maps to a model, a machine and a budget. When the developer configured a list, only those names."
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"type": "string",
|
|
76
76
|
"maxLength": 120,
|
|
77
77
|
"not": {
|
|
78
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
78
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
79
79
|
},
|
|
80
80
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$"
|
|
81
81
|
},
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"type": "string",
|
|
119
119
|
"maxLength": 200,
|
|
120
120
|
"not": {
|
|
121
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
121
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
122
122
|
},
|
|
123
123
|
"minLength": 1,
|
|
124
124
|
"description": "A hint: a safe git ref name.",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"type": "string",
|
|
138
138
|
"maxLength": 20000,
|
|
139
139
|
"not": {
|
|
140
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
140
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
143
|
"limits": {
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"type": "string",
|
|
166
166
|
"maxLength": 200,
|
|
167
167
|
"not": {
|
|
168
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
168
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
169
169
|
},
|
|
170
170
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$"
|
|
171
171
|
}
|
|
@@ -174,13 +174,44 @@
|
|
|
174
174
|
"type": "string",
|
|
175
175
|
"maxLength": 40,
|
|
176
176
|
"not": {
|
|
177
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
177
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
178
178
|
},
|
|
179
179
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$"
|
|
180
180
|
},
|
|
181
181
|
"requestedAt": {
|
|
182
182
|
"type": "string",
|
|
183
183
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$"
|
|
184
|
+
},
|
|
185
|
+
"trigger": {
|
|
186
|
+
"description": "Only on an automatic request (agents.on): what caused it. A maintainer's act, never a run's output.",
|
|
187
|
+
"type": "object",
|
|
188
|
+
"additionalProperties": false,
|
|
189
|
+
"required": [
|
|
190
|
+
"event",
|
|
191
|
+
"by",
|
|
192
|
+
"ref"
|
|
193
|
+
],
|
|
194
|
+
"properties": {
|
|
195
|
+
"event": {
|
|
196
|
+
"enum": [
|
|
197
|
+
"spec.approved",
|
|
198
|
+
"build.opened",
|
|
199
|
+
"thread.message"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
"by": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"maxLength": 40,
|
|
205
|
+
"not": {
|
|
206
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
207
|
+
},
|
|
208
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$"
|
|
209
|
+
},
|
|
210
|
+
"ref": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"pattern": "^(pull|issue|comment):[0-9]{1,20}$"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
184
215
|
}
|
|
185
216
|
}
|
|
186
217
|
},
|
|
@@ -205,14 +236,14 @@
|
|
|
205
236
|
"type": "string",
|
|
206
237
|
"maxLength": 4000,
|
|
207
238
|
"not": {
|
|
208
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
239
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
209
240
|
}
|
|
210
241
|
},
|
|
211
242
|
"url": {
|
|
212
243
|
"type": "string",
|
|
213
244
|
"maxLength": 2000,
|
|
214
245
|
"not": {
|
|
215
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
246
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
216
247
|
},
|
|
217
248
|
"pattern": "^https?://[^\\s/@?#]+([/?#]\\S*)?$",
|
|
218
249
|
"description": "Where the runner keeps its logs.",
|
|
@@ -250,7 +281,7 @@
|
|
|
250
281
|
"type": "string",
|
|
251
282
|
"maxLength": 4000,
|
|
252
283
|
"not": {
|
|
253
|
-
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
284
|
+
"pattern": "(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{20,}|(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{15,}|(?<![A-Za-z0-9])gto-[A-Za-z0-9]{20,}|(?<![A-Za-z0-9])cf_(?:rt|at|nt)_[A-Za-z0-9_-]{20,}|(?<![A-Za-z0-9])sk-(?=[A-Za-z0-9_-]{20})[A-Za-z_-]{0,40}[0-9]|(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}|(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}|(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{10,}|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}|-----BEGIN [A-Z ]{0,40}PRIVATE KEY-----|(?<![A-Za-z0-9])[bB][eE][aA][rR][eE][rR][ \\t]+(?=[A-Za-z0-9._~+/=-]{20})[A-Za-z._~+/=-]{0,40}[0-9]|(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]{0,31}://[^\\s/@:]+:[^\\s/@]+@"
|
|
254
285
|
}
|
|
255
286
|
}
|
|
256
287
|
}
|
package/core/config.js
CHANGED
|
@@ -40,7 +40,9 @@ import fs from 'node:fs';
|
|
|
40
40
|
import os from 'node:os';
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
43
|
-
import { PROFILE_SOURCE, MAX_NUMBER } from './agent-contract.js';
|
|
43
|
+
import { PROFILE_SOURCE, MAX_NUMBER, MAX_BRIEF_CHARS, RUN_KINDS, TRIGGER_EVENTS, CREDENTIAL_VALUE_SOURCE } from './agent-contract.js';
|
|
44
|
+
|
|
45
|
+
export { TRIGGER_EVENTS };
|
|
44
46
|
|
|
45
47
|
export const CONFIG_FILE = 'cornfield.config.js';
|
|
46
48
|
export const CORNFIELD_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -68,7 +70,7 @@ export const FORGE_TYPES = ['github', 'forgejo'];
|
|
|
68
70
|
/** How a pull request cornfield lands is merged (`forge.mergeMethod`): the way the repo lands its pull requests */
|
|
69
71
|
export const MERGE_METHODS = ['merge', 'squash', 'rebase'];
|
|
70
72
|
/** The events a hook may ask for (`hooks[].events`), and what each one means: docs/config.md#hooks */
|
|
71
|
-
export const HOOK_EVENTS = Object.freeze(['spec.ready', 'spec.approved', 'spec.sentBack', 'build.opened', 'pr.landed', 'session.expiring', 'thread.opened', 'thread.message', 'thread.linked', 'agent.requested', 'run.claimed', 'run.state', 'run.stopRequested', 'run.finished']);
|
|
73
|
+
export const HOOK_EVENTS = Object.freeze(['spec.ready', 'spec.approved', 'spec.sentBack', 'build.opened', 'pr.landed', 'session.expiring', 'thread.opened', 'thread.message', 'thread.linked', 'agent.requested', 'run.claimed', 'run.state', 'run.stopRequested', 'run.finished', 'trigger.skipped']);
|
|
72
74
|
/** The events that come from looking at threads (server/threads.js): a poll that wants any of them looks at threads too */
|
|
73
75
|
export const THREAD_EVENTS = Object.freeze(['thread.opened', 'thread.message', 'thread.linked']);
|
|
74
76
|
/** The keys of `threads` when it is an object, and the label's longest length */
|
|
@@ -111,13 +113,13 @@ export const ideaLabel = (config) => (threadsOn(config) ? (config.threads && con
|
|
|
111
113
|
* @returns {number | null}
|
|
112
114
|
*/
|
|
113
115
|
export function specPollMs(config) {
|
|
114
|
-
const wanted = hookWants(config, ['spec.ready']) || (threadsOn(config) && hookWants(config, THREAD_EVENTS));
|
|
116
|
+
const wanted = hookWants(config, ['spec.ready']) || (threadsOn(config) && (hookWants(config, THREAD_EVENTS) || triggersOf(config).some(([e]) => e === 'thread.message')));
|
|
115
117
|
const every = config.hookPoll === false ? false : config.hookPoll ? config.hookPoll.specs : DEFAULTS.hookPoll.specs;
|
|
116
118
|
return wanted && every !== false ? parseDuration(every ?? DEFAULTS.hookPoll.specs) : null;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/** How a request reaches a runner: the forge
|
|
120
|
-
export const AGENT_TRANSPORTS = Object.freeze(['forge']);
|
|
121
|
+
/** How a request reaches a runner (spec 0005 D2): posted on the forge (the default), pushed as a signed `agent.requested` hook event, or kept for a runner to pull and claim */
|
|
122
|
+
export const AGENT_TRANSPORTS = Object.freeze(['forge', 'push', 'pull']);
|
|
121
123
|
/** `agents.maxOpen`: the most open runs per repo cornfield will ask for, by default and at most */
|
|
122
124
|
export const DEFAULT_MAX_OPEN = 3;
|
|
123
125
|
export const MAX_OPEN_CEILING = 100;
|
|
@@ -150,7 +152,7 @@ export const DEFAULTS = Object.freeze({
|
|
|
150
152
|
personas: { roles: null, parent: 'root' },
|
|
151
153
|
cloud: { parent: 'root' },
|
|
152
154
|
releaseNotes: { breakingLabel: 'breaking' },
|
|
153
|
-
agents: { authors: [], labels: [], branchPrefixes: [], bots: true, transport: 'forge', maxOpen: DEFAULT_MAX_OPEN, profiles: [], runners: [], quietAfter: DEFAULT_QUIET_AFTER, limits: {} },
|
|
155
|
+
agents: { authors: [], labels: [], branchPrefixes: [], bots: true, transport: 'forge', maxOpen: DEFAULT_MAX_OPEN, profiles: [], runners: [], quietAfter: DEFAULT_QUIET_AFTER, limits: {}, on: {} },
|
|
154
156
|
hooks: [],
|
|
155
157
|
hookPoll: { specs: '5m' },
|
|
156
158
|
threads: { label: 'idea' },
|
|
@@ -163,7 +165,7 @@ export const DEFAULTS = Object.freeze({
|
|
|
163
165
|
const KEYS = Object.keys(DEFAULTS);
|
|
164
166
|
/** The keys of `proposals` when it is an object: the folder, and whether an approval also lands the spec PR (opt-in, off unless true) */
|
|
165
167
|
export const PROPOSALS_KEYS = Object.freeze(['dir', 'approveLands']);
|
|
166
|
-
const AGENT_KEYS = ['authors', 'labels', 'branchPrefixes', 'bots', 'transport', 'maxOpen', 'profiles', 'runners', 'quietAfter', 'limits'];
|
|
168
|
+
const AGENT_KEYS = ['authors', 'labels', 'branchPrefixes', 'bots', 'transport', 'maxOpen', 'profiles', 'runners', 'quietAfter', 'limits', 'on'];
|
|
167
169
|
const PROFILE_NAME = new RegExp(PROFILE_SOURCE);
|
|
168
170
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
169
171
|
const isText = (v) => typeof v === 'string' && v.trim() !== '';
|
|
@@ -237,6 +239,7 @@ export function configProblems(c) {
|
|
|
237
239
|
}
|
|
238
240
|
if (c.agents.bots !== undefined && typeof c.agents.bots !== 'boolean') out.push('agents.bots is true or false: whether a bot account ("[bot]" or the forge\'s Bot type) counts as an agent.');
|
|
239
241
|
out.push(...agentRunProblems(c.agents));
|
|
242
|
+
if (isObject(c.agents.on) && c.agents.on['thread.message'] !== undefined && c.threads === false) out.push('agents.on["thread.message"] needs threads, which are off (threads: false): the /agent command is read in a thread.');
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
245
|
if (c.deploys !== undefined && c.deploys !== null) {
|
|
@@ -269,6 +272,10 @@ export function configProblems(c) {
|
|
|
269
272
|
});
|
|
270
273
|
}
|
|
271
274
|
}
|
|
275
|
+
// Push is the hook's delivery: with no hook that wants agent.requested the request would go nowhere
|
|
276
|
+
if (isObject(c.agents) && c.agents.transport === 'push' && !hookWants(c, ['agent.requested'])) {
|
|
277
|
+
out.push('agents.transport is "push", so a hook must subscribe to agent.requested: add { url, events: ["agent.requested"] } (or "*") to "hooks". Without one a request would go nowhere.');
|
|
278
|
+
}
|
|
272
279
|
if (c.hookPoll !== undefined && c.hookPoll !== false) {
|
|
273
280
|
if (!isObject(c.hookPoll)) out.push('"hookPoll" is { specs: "5m" }, or false for none.');
|
|
274
281
|
else {
|
|
@@ -328,10 +335,34 @@ export function configProblems(c) {
|
|
|
328
335
|
return out;
|
|
329
336
|
}
|
|
330
337
|
|
|
338
|
+
/**
|
|
339
|
+
* What is wrong with `agents.on`, the opt-in triggers (spec 0005 D10): `{ '<event>': { kind, profile?, brief? } }` for the events a
|
|
340
|
+
* maintainer causes. Off by default (no key, no trigger). `kind` is checked against the contract and `profile` against `agents.profiles`.
|
|
341
|
+
*/
|
|
342
|
+
function triggerProblems(a) {
|
|
343
|
+
const out = [];
|
|
344
|
+
const on = a.on;
|
|
345
|
+
if (!isObject(on)) return [`agents.on is { ${TRIGGER_EVENTS.map((e) => `"${e}": { kind, profile? }`).join(', ')} }: which events request a run by themselves (none unless set).`];
|
|
346
|
+
const profiles = Array.isArray(a.profiles) ? a.profiles : [];
|
|
347
|
+
for (const [event, t] of Object.entries(on)) {
|
|
348
|
+
const at = `agents.on[${JSON.stringify(event)}]`;
|
|
349
|
+
if (!TRIGGER_EVENTS.includes(event)) { out.push(`${at}: a trigger fires only on ${TRIGGER_EVENTS.join(', ')}: something a maintainer did, never a run's output.`); continue; }
|
|
350
|
+
if (!isObject(t)) { out.push(`${at} is { kind, profile?, brief? }.`); continue; }
|
|
351
|
+
for (const k of Object.keys(t)) if (!['kind', 'profile', 'brief'].includes(k)) out.push(`${at}.${k}: the keys are kind, profile, brief.`);
|
|
352
|
+
if (!RUN_KINDS.includes(t.kind)) out.push(`${at}.kind is one of ${RUN_KINDS.join(', ')}.`);
|
|
353
|
+
if (t.profile !== undefined && !(typeof t.profile === 'string' && profiles.includes(t.profile))) out.push(`${at}.profile ${JSON.stringify(t.profile)} is not one of agents.profiles (${profiles.join(', ') || 'none listed'}).`);
|
|
354
|
+
if (t.brief !== undefined && !(typeof t.brief === 'string' && t.brief.length <= MAX_BRIEF_CHARS && !new RegExp(CREDENTIAL_VALUE_SOURCE).test(t.brief))) out.push(`${at}.brief is text of at most ${MAX_BRIEF_CHARS} characters, and holds nothing shaped like a credential.`);
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The triggers a config turns on, as `[event, { kind, profile?, brief? }]` (none when off, which is the default) */
|
|
360
|
+
export const triggersOf = (config) => Object.entries(isObject(config.agents?.on) ? config.agents.on : {}).filter(([e]) => TRIGGER_EVENTS.includes(e));
|
|
361
|
+
|
|
331
362
|
/** What is wrong with the keys of `agents` that ask for and follow runs (spec 0005), in words */
|
|
332
363
|
function agentRunProblems(a) {
|
|
333
364
|
const out = [];
|
|
334
|
-
if (a.transport !== undefined && !AGENT_TRANSPORTS.includes(a.transport)) out.push(`agents.transport is ${AGENT_TRANSPORTS.map((t) => JSON.stringify(t)).join('
|
|
365
|
+
if (a.transport !== undefined && !AGENT_TRANSPORTS.includes(a.transport)) out.push(`agents.transport is ${AGENT_TRANSPORTS.map((t) => JSON.stringify(t)).join(', ')}: where a request goes: "forge" posts it as a comment (the default), "push" sends it as an agent.requested hook event, "pull" keeps it for a runner to claim (docs/agents.md#transports).`);
|
|
335
366
|
if (a.maxOpen !== undefined && !(Number.isInteger(a.maxOpen) && a.maxOpen >= 1 && a.maxOpen <= MAX_OPEN_CEILING)) out.push(`agents.maxOpen is a whole number from 1 to ${MAX_OPEN_CEILING}: the most open runs cornfield asks for at once (${DEFAULT_MAX_OPEN} unless set).`);
|
|
336
367
|
if (a.profiles !== undefined) {
|
|
337
368
|
if (!Array.isArray(a.profiles) || a.profiles.length > MAX_PROFILES || !a.profiles.every((p) => typeof p === 'string' && PROFILE_NAME.test(p)) || new Set(a.profiles).size !== a.profiles.length) {
|
|
@@ -349,6 +380,7 @@ function agentRunProblems(a) {
|
|
|
349
380
|
else if (ms < QUIET_MIN_MS) out.push(`agents.quietAfter ${JSON.stringify(a.quietAfter)} is under the floor of 5m; use "5m" or more.`);
|
|
350
381
|
else if (ms > QUIET_MAX_MS) out.push(`agents.quietAfter ${JSON.stringify(a.quietAfter)} is over the ceiling of 30 days; use "720h" or less.`);
|
|
351
382
|
}
|
|
383
|
+
if (a.on !== undefined) out.push(...triggerProblems(a));
|
|
352
384
|
if (a.limits !== undefined) {
|
|
353
385
|
if (!isObject(a.limits)) out.push('agents.limits is { minutes?, turns?, cost? }: hints the runner may enforce.');
|
|
354
386
|
else {
|
package/explorer/forge-panel.js
CHANGED
|
@@ -69,7 +69,7 @@ import { relativeTime } from './review-model.js';
|
|
|
69
69
|
import { LAYER_DEFS, toggleLayer } from './layers.js';
|
|
70
70
|
import { WORK_URL, groupWork, filterGroups, groupWords, workStatus, markWords } from './agents-model.js';
|
|
71
71
|
import {
|
|
72
|
-
RUNS_URL, runsUrl, stopUrl, KINDS, aboutWords, statusWords, statusWhy, runMeta, canStop, runsStatus, startBody, startWords, stopWords, confirmStartWords,
|
|
72
|
+
RUNS_URL, runsUrl, stopUrl, KINDS, aboutWords, statusWords, statusWhy, runMeta, canStop, runsStatus, triggerNotice, startBody, startWords, stopWords, confirmStartWords,
|
|
73
73
|
} from './runs-model.js';
|
|
74
74
|
import {
|
|
75
75
|
STATES, defaultFilters, listUrl, pullUrl, serverState, filterItems, facetsOf, stateWords, metaWords, checkWords, checksWords, litFor, errorWords,
|
|
@@ -1002,6 +1002,12 @@ function paintRuns() {
|
|
|
1002
1002
|
note.hidden = !words;
|
|
1003
1003
|
note.className = `hx-forge-notice${!(ui.runs && ui.runs.error) && ui.runNote && ui.runNote.ok ? ' hx-forge-notice--ok' : ''}`;
|
|
1004
1004
|
note.textContent = words;
|
|
1005
|
+
const trig = $('forge-runs-trigger');
|
|
1006
|
+
if (trig) {
|
|
1007
|
+
const skipped = data ? triggerNotice(data) : '';
|
|
1008
|
+
trig.hidden = !skipped;
|
|
1009
|
+
trig.textContent = skipped;
|
|
1010
|
+
}
|
|
1005
1011
|
const list = $('forge-runs-list');
|
|
1006
1012
|
if (!data) { list.innerHTML = ''; return; }
|
|
1007
1013
|
list.innerHTML = data.runs.length ? data.runs.map(runHtml).join('') : '<li class="hx-forge-empty hx-faint">No agent has been asked for yet. Start one from an issue or a proposal.</li>';
|
package/explorer/index.html
CHANGED
|
@@ -270,6 +270,7 @@
|
|
|
270
270
|
<h3 id="forge-runs-title" class="hx-forge-runs-title">Agent runs</h3>
|
|
271
271
|
<p id="forge-runs-status" class="hx-forge-meta hx-faint"></p>
|
|
272
272
|
<p id="forge-runs-note" class="hx-forge-notice" hidden></p>
|
|
273
|
+
<p id="forge-runs-trigger" class="hx-forge-notice" hidden></p>
|
|
273
274
|
<ul id="forge-runs-list" class="hx-forge-files" aria-label="Runs"></ul>
|
|
274
275
|
</section>
|
|
275
276
|
<section id="forge-detail" class="hx-forge-detail" aria-label="Selected pull request" hidden></section>
|
|
@@ -381,7 +382,7 @@
|
|
|
381
382
|
</div>
|
|
382
383
|
<ul id="tokens-list" class="hx-tokens-list" aria-label="Your review tokens"></ul>
|
|
383
384
|
<form id="tokens-form" class="hx-tokens-form" autocomplete="off">
|
|
384
|
-
<label class="hx-forge-field">Kind of token<select id="tokens-scope" class="hx-forge-select"><option value="review">Review: approve or send back proposals</option><option value="agent">Agent: ask for agent runs (POST /api/agent-requests)</option></select></label>
|
|
385
|
+
<label class="hx-forge-field">Kind of token<select id="tokens-scope" class="hx-forge-select"><option value="review">Review: approve or send back proposals</option><option value="agent">Agent: ask for agent runs (POST /api/agent-requests)</option><option value="runner">Runner: pull, claim and report on runs (GET /api/agent-requests)</option></select></label>
|
|
385
386
|
<label class="hx-forge-field">Label<input id="tokens-label" class="hx-forge-input" type="text" maxlength="60" placeholder="chat bot" /></label>
|
|
386
387
|
<label class="hx-forge-field">Days<input id="tokens-days" class="hx-forge-input" type="text" inputmode="numeric" maxlength="2" aria-describedby="tokens-days-hint" /></label>
|
|
387
388
|
<span id="tokens-days-hint" class="hx-sr-only"></span>
|
|
@@ -25,9 +25,16 @@ const DAY = 24 * 60 * 60 * 1000;
|
|
|
25
25
|
const HOUR = 60 * 60 * 1000;
|
|
26
26
|
const LABEL = /^[\p{L}\p{N}][\p{L}\p{N} ._:/()-]*$/u;
|
|
27
27
|
|
|
28
|
-
/** Tokens come in
|
|
29
|
-
export const SCOPES = Object.freeze([['review', 'Review: approve or send back proposals'], ['agent', 'Agent: ask for agent runs']]);
|
|
30
|
-
export const tokensUrl = (scope) => (scope === 'agent' ? '/api/agent-tokens' : TOKENS_URL);
|
|
28
|
+
/** Tokens come in three scopes (server/review-tokens.js): `review` approves proposals, `agent` asks for agent runs, `runner` pulls and reports on them. Each has its own address. */
|
|
29
|
+
export const SCOPES = Object.freeze([['review', 'Review: approve or send back proposals'], ['agent', 'Agent: ask for agent runs'], ['runner', 'Runner: pull, claim and report on runs']]);
|
|
30
|
+
export const tokensUrl = (scope) => (scope === 'agent' ? '/api/agent-tokens' : scope === 'runner' ? '/api/runner-tokens' : TOKENS_URL);
|
|
31
|
+
|
|
32
|
+
/** What the dialog says a token of this scope is for */
|
|
33
|
+
export function introWords(scope) {
|
|
34
|
+
if (scope === 'agent') return 'An agent token lets a script or a chat bot ask for an agent run as you (POST /api/agent-requests). It works while you are signed in here and still maintain the repo; it expires, and you can revoke it below.';
|
|
35
|
+
if (scope === 'runner') return 'A runner token lets your own runner, wherever it runs, list the queued requests, claim one and report on it (GET /api/agent-requests, POST /api/agent-requests/<run>/claim, POST /api/runs/<run>). It reaches nothing else, and works until it expires or you revoke it below, even after you sign out.';
|
|
36
|
+
return 'A review token lets a script or a chat bot approve or send back a proposal as you (POST /api/reviews). It works while you are signed in here and still maintain the repo; it expires, and you can revoke it below.';
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
/** The address that revokes token `id` */
|
|
33
40
|
export const revokeUrl = (id, scope = 'review') => `${tokensUrl(scope)}/${encodeURIComponent(String(id))}/revoke`;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { escHtml } from './state.js';
|
|
22
22
|
import { announce } from './keyboard.js';
|
|
23
|
-
import { tokensUrl, DEFAULT_DAYS, MAX_DAYS, makeRequest, revokeUrl, tokenLine, errorWords } from './review-tokens-model.js';
|
|
23
|
+
import { tokensUrl, introWords, DEFAULT_DAYS, MAX_DAYS, makeRequest, revokeUrl, tokenLine, errorWords } from './review-tokens-model.js';
|
|
24
24
|
|
|
25
25
|
const $ = (id) => document.getElementById(id);
|
|
26
26
|
let wired = false;
|
|
@@ -58,6 +58,8 @@ function paint() {
|
|
|
58
58
|
return `<li class="hx-tokens-item" data-state="${line.state}"><span class="hx-tokens-label">${escHtml(line.label)}</span> <span class="hx-tokens-meta hx-faint">${escHtml(line.status)} · ${escHtml(line.used)}</span> ${line.canRevoke ? `<button type="button" class="hx-btn hx-btn--quiet" data-revoke="${escHtml(String(line.id))}" aria-label="Revoke ${escHtml(line.label)}"${ui.busy ? ' disabled' : ''}>Revoke</button>` : ''}</li>`;
|
|
59
59
|
}).join('');
|
|
60
60
|
}
|
|
61
|
+
const intro = $('tokens-intro');
|
|
62
|
+
if (intro) intro.textContent = introWords(ui.scope);
|
|
61
63
|
const msg = $('tokens-msg');
|
|
62
64
|
msg.hidden = !ui.message;
|
|
63
65
|
msg.textContent = ui.message ? ui.message.text : '';
|
|
@@ -204,7 +206,7 @@ export function initReviewTokens(config, opts = {}) {
|
|
|
204
206
|
dialog().addEventListener('keydown', onKey);
|
|
205
207
|
$('tokens-form').addEventListener('submit', make);
|
|
206
208
|
const scope = $('tokens-scope');
|
|
207
|
-
if (scope) scope.addEventListener('change', () => { ui.scope = scope.value === 'agent'
|
|
209
|
+
if (scope) scope.addEventListener('change', () => { ui.scope = scope.value === 'agent' || scope.value === 'runner' ? scope.value : 'review'; ui.loaded = false; ui.tokens = []; clearSecret(); paint(); load(); });
|
|
208
210
|
}
|
|
209
211
|
return true;
|
|
210
212
|
}
|
package/explorer/runs-model.js
CHANGED
|
@@ -64,6 +64,24 @@ export function runMeta(run, nowMs) {
|
|
|
64
64
|
return parts.join(' · ');
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The notice over the runs when an opt-in trigger (`agents.on`) did nothing in the last day, from the audit rows the server counted
|
|
69
|
+
* (`triggerSkipped`: { count, reasons }), or '' when none did. No live sign-in is the case the person can mend, so it says how.
|
|
70
|
+
*/
|
|
71
|
+
export function triggerNotice(data) {
|
|
72
|
+
const n = data && data.triggerSkipped;
|
|
73
|
+
if (!n || !(n.count > 0)) return '';
|
|
74
|
+
const reasons = n.reasons || {};
|
|
75
|
+
const out = [];
|
|
76
|
+
if (reasons['no-session']) out.push('A trigger was skipped: sign in to let triggers request runs.');
|
|
77
|
+
const refused = Object.entries(reasons).filter(([why]) => why !== 'no-session');
|
|
78
|
+
if (refused.length) {
|
|
79
|
+
const count = refused.reduce((sum, [, c]) => sum + c, 0);
|
|
80
|
+
out.push(`${count} ${count === 1 ? 'trigger was' : 'triggers were'} refused in the last day (${refused.map(([why, c]) => `${why} ${c}`).join(', ')}), so nothing was asked.`);
|
|
81
|
+
}
|
|
82
|
+
return out.join(' ');
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
/** Whether a Stop is offered: the run is open and nobody has asked it to stop yet */
|
|
68
86
|
export const canStop = (run) => run.open === true && !run.stopRequested;
|
|
69
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popcorn245/cornfield",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A self-hostable dev dashboard with the topology map at its centre: proposals on the map, scenarios, outages and what-if edits, for any repo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "popcorn245 (https://github.com/popcorn245)",
|
package/server/agent-api.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* The routes that ask for agent runs and show them (issue #80, spec 0005 D10; the engine is server/agent-runs.js).
|
|
5
5
|
*
|
|
6
6
|
* POST /api/agent-requests { kind, refs, profile?, brief?, branch?, limits?, nodes?, force? }, with `Authorization: Bearer cf_at_…`
|
|
7
|
+
* GET /api/agent-requests the queue, for a runner token: server/runner-api.js (and the pull transport's claim and report routes)
|
|
7
8
|
* GET /api/agent-tokens the signed-in maintainer's agent tokens (and POST, and /<id>/revoke: server/review-api.js)
|
|
8
9
|
* GET /api/agent-runs[?follow=1] the runs, newest first, with the cap and how many are open; `follow=1` looks at the forge first
|
|
9
10
|
* GET /api/agent-runs/<runId> one run
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
* **The call takes an agent token and nothing else,** exactly as the review API takes a review token (server/review-api.js): it runs
|
|
14
15
|
* before the sign-in guard (a script has no cookie), reads no cookie, needs no same-origin check (a browser never adds an `Authorization`
|
|
15
16
|
* header itself), and keeps the `Host` check in front of every route. **The two scopes are kept apart:** a review token on this route is 403
|
|
16
|
-
* (and an agent token on `POST /api/reviews` is 403), whatever else is true of it. Token states, the anonymous flood ceiling and the rows
|
|
17
|
+
* (and an agent token on `POST /api/reviews` is 403, and a runner token on either), whatever else is true of it. Token states, the anonymous flood ceiling and the rows
|
|
17
18
|
* in `actions` are the review API's, under the action `agent-request`:
|
|
18
19
|
*
|
|
19
20
|
* no token, or one that no row has 401 (`WWW-Authenticate: Bearer`), logged under `(no token)`; past `ANONYMOUS_PER_MINUTE` a minute 429 and no row
|
|
@@ -84,7 +85,7 @@ export async function serveAgentCall(req, res, ctx) {
|
|
|
84
85
|
}
|
|
85
86
|
if (req.method !== 'POST') {
|
|
86
87
|
req.resume();
|
|
87
|
-
return send(res, 405, { ok: false, error: 'A run is asked for with POST.', code: 'invalid' }, { Allow: 'POST' });
|
|
88
|
+
return send(res, 405, { ok: false, error: 'A run is asked for with POST (an agent token); a runner lists the queue with GET (a runner token).', code: 'invalid' }, { Allow: 'GET, HEAD, POST' });
|
|
88
89
|
}
|
|
89
90
|
const repo = config.forge.repo;
|
|
90
91
|
const audit = async (login, request, status, body) => {
|
|
@@ -115,7 +116,7 @@ export async function serveAgentCall(req, res, ctx) {
|
|
|
115
116
|
if (flood) return send(res, flood.status, { ok: false, error: flood.message, code: flood.code }, flood.headers);
|
|
116
117
|
const wrong = state === 'wrong-scope';
|
|
117
118
|
const body = {
|
|
118
|
-
error: wrong ? `This is a ${row.scope} token. Asking for a run takes an agent token; make one in the dashboard.`
|
|
119
|
+
error: wrong ? `This is ${row.scope === 'agent' ? 'an' : 'a'} ${row.scope} token. Asking for a run takes an agent token; make one in the dashboard.`
|
|
119
120
|
: state === 'revoked' ? 'This agent token was revoked; make a new one in the dashboard.' : 'This agent token has expired; make a new one in the dashboard.',
|
|
120
121
|
code: 'forbidden',
|
|
121
122
|
};
|