@gaia-ai/conductor 0.9.2 → 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/dist/src/cli/config-schema.d.ts +52 -3
- package/dist/src/cli/config-schema.js +231 -3
- package/dist/src/cli/init.d.ts +4 -2
- package/dist/src/cli/init.js +45 -23
- package/dist/src/commands/conductor.js +14 -8
- package/dist/src/config.js +43 -13
- package/dist/src/core/conductor.d.ts +6 -0
- package/dist/src/core/conductor.js +45 -8
- package/dist/src/plugins/remote.d.ts +13 -0
- package/dist/src/plugins/workspace.d.ts +1 -0
- package/dist/src/types.d.ts +18 -5
- package/package.json +5 -5
|
@@ -43,11 +43,60 @@ export declare const RENDERER_PLUGIN_ENTRY = "{ plugin: '@gaia-ai/addon-essentia
|
|
|
43
43
|
* hand-reshaped file) is left alone rather than guessed at.
|
|
44
44
|
*/
|
|
45
45
|
export declare function addRendererPluginEntry(text: string): string;
|
|
46
|
+
/** The dropsh plugin package whose entries carry a connection's OAuth identity. */
|
|
47
|
+
export declare const OAUTH2_PLUGIN_PACKAGE = "@dropsh/plugin-oauth2";
|
|
48
|
+
/**
|
|
49
|
+
* The id of the ONE oauth2 provider a GAIA connection declares (GAIA-391 D1).
|
|
50
|
+
*
|
|
51
|
+
* `gaia`, not `session`: the entry names the CONNECTION, not a capability. Until
|
|
52
|
+
* v3 a config declared two providers that differed only by scope — `session`
|
|
53
|
+
* (`gaia:session`) and `pm` (`gaia:project_manager`) — and every write had to
|
|
54
|
+
* pick one. What that choice cost is what GAIA-391 removes; the id therefore
|
|
55
|
+
* names the connection, and the surviving scope carries the capability.
|
|
56
|
+
*/
|
|
57
|
+
export declare const OAUTH2_PROVIDER_ID = "gaia";
|
|
58
|
+
/**
|
|
59
|
+
* The ONE OAuth2 scope a GAIA connection requests (GAIA-391 D1, as revised).
|
|
60
|
+
*
|
|
61
|
+
* There is exactly one, and there is not zero. `gaia:project_manager` is
|
|
62
|
+
* retired; `gaia:session` survives and its role carries the ceiling of what a
|
|
63
|
+
* machine identity may do.
|
|
64
|
+
*
|
|
65
|
+
* Zero was the original design, and a live control plane refuted it:
|
|
66
|
+
* `simple_oauth`'s Oauth2AccessPolicy rewrites a token account's permissions
|
|
67
|
+
* with `overwrite: TRUE`, and for a `client_credentials` token — every GAIA
|
|
68
|
+
* machine identity — the set becomes EXACTLY the scope's permissions, with
|
|
69
|
+
* neither user_role nor OG-rank permissions surviving. A request naming no scope
|
|
70
|
+
* against a consumer holding no default scopes does not even mint a token.
|
|
71
|
+
*/
|
|
72
|
+
export declare const OAUTH2_SCOPE = "gaia:session";
|
|
73
|
+
/**
|
|
74
|
+
* GAIA-391 (v2 → v3): collapse a connection's oauth2 entries into ONE `gaia`
|
|
75
|
+
* provider on ONE scope — the whole of the one-identity change, as a pure text
|
|
76
|
+
* transform.
|
|
77
|
+
*
|
|
78
|
+
* A MIGRATION rather than a cutover (spec D3): operators have hand-edited these
|
|
79
|
+
* files — a pinned literal `baseUrl`, a per-install `workspace_id`, an extra
|
|
80
|
+
* `addons[]` entry — so every value outside the two elements being merged is
|
|
81
|
+
* carried over verbatim, and the runner never EXECUTES a possibly
|
|
82
|
+
* side-effecting config module.
|
|
83
|
+
*
|
|
84
|
+
* The FIRST oauth2 element survives (it is the one v2 marked `default: true`);
|
|
85
|
+
* the rest are removed line-and-all. A config with no oauth2 element is left
|
|
86
|
+
* exactly as it is — that is the one silent no-op, and it is silent because
|
|
87
|
+
* there is nothing to collapse. An element that IS there but cannot be given the
|
|
88
|
+
* `gaia` identity throws (see {@link ensureProviderId}) rather than passing
|
|
89
|
+
* through unchanged.
|
|
90
|
+
*/
|
|
91
|
+
export declare function collapseOauth2Providers(text: string): string;
|
|
46
92
|
/**
|
|
47
93
|
* The ordered, contiguous migration chain. v1 was the pre-GAIA-226 shape (auth
|
|
48
|
-
* plugins only); v2 adds the markdown renderer entry
|
|
49
|
-
*
|
|
50
|
-
*
|
|
94
|
+
* plugins only); v2 adds the markdown renderer entry; v3 collapses the two
|
|
95
|
+
* differently-scoped oauth2 providers into ONE identity on ONE scope (GAIA-391)
|
|
96
|
+
* — one, not zero: see {@link OAUTH2_SCOPE} for why the scope survives.
|
|
97
|
+
* Nothing below v1 is a versioned config (v0 = seed / hand-authored / untouched).
|
|
98
|
+
* `GAIA_CONFIG_SCHEMA_VERSION` derives from the chain's last `to` — never
|
|
99
|
+
* hardcode it.
|
|
51
100
|
*/
|
|
52
101
|
export declare const CONNECTION_MIGRATIONS: ConnectionConfigMigration[];
|
|
53
102
|
/** The current connection-config schema version — single source of truth. */
|
|
@@ -27,6 +27,19 @@ export const RENDERER_PLUGIN_ENTRY = `{ plugin: '${RENDERER_PLUGIN_PACKAGE}' },`
|
|
|
27
27
|
const PLUGINS_ARRAY_OPEN_RE = /^([ \t]*)plugins:\s*\[[ \t]*$/m;
|
|
28
28
|
/** Whole-line `//` comments — stripped before the presence probe (see below). */
|
|
29
29
|
const LINE_COMMENT_RE = /^[ \t]*\/\/.*$/gm;
|
|
30
|
+
/**
|
|
31
|
+
* Blank out whole-line `//` comments WITHOUT moving a single byte.
|
|
32
|
+
*
|
|
33
|
+
* `addRendererPluginEntry` can simply delete them, because it only asks a
|
|
34
|
+
* yes/no question. The v2→v3 collapse below needs OFFSETS into the real text,
|
|
35
|
+
* so a comment has to become the same number of spaces rather than disappear:
|
|
36
|
+
* the shipped connection config explains itself in prose that names both
|
|
37
|
+
* `@dropsh/plugin-oauth2` and a `{ site, plugins }` shape, and a scan that read
|
|
38
|
+
* those as code would splice a comment fragment.
|
|
39
|
+
*/
|
|
40
|
+
function maskLineComments(text) {
|
|
41
|
+
return text.replace(LINE_COMMENT_RE, (line) => ' '.repeat(line.length));
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* GAIA-226 (v1 → v2): insert the renderer aggregator as the FIRST `plugins[]`
|
|
32
45
|
* element, so `gaia dropsh --format md` works instead of failing with
|
|
@@ -49,11 +62,219 @@ export function addRendererPluginEntry(text) {
|
|
|
49
62
|
const indent = `${m[1]} `;
|
|
50
63
|
return text.replace(PLUGINS_ARRAY_OPEN_RE, (line) => `${line}\n${indent}${RENDERER_PLUGIN_ENTRY}`);
|
|
51
64
|
}
|
|
65
|
+
/** The dropsh plugin package whose entries carry a connection's OAuth identity. */
|
|
66
|
+
export const OAUTH2_PLUGIN_PACKAGE = '@dropsh/plugin-oauth2';
|
|
67
|
+
/**
|
|
68
|
+
* The id of the ONE oauth2 provider a GAIA connection declares (GAIA-391 D1).
|
|
69
|
+
*
|
|
70
|
+
* `gaia`, not `session`: the entry names the CONNECTION, not a capability. Until
|
|
71
|
+
* v3 a config declared two providers that differed only by scope — `session`
|
|
72
|
+
* (`gaia:session`) and `pm` (`gaia:project_manager`) — and every write had to
|
|
73
|
+
* pick one. What that choice cost is what GAIA-391 removes; the id therefore
|
|
74
|
+
* names the connection, and the surviving scope carries the capability.
|
|
75
|
+
*/
|
|
76
|
+
export const OAUTH2_PROVIDER_ID = 'gaia';
|
|
77
|
+
/**
|
|
78
|
+
* The ONE OAuth2 scope a GAIA connection requests (GAIA-391 D1, as revised).
|
|
79
|
+
*
|
|
80
|
+
* There is exactly one, and there is not zero. `gaia:project_manager` is
|
|
81
|
+
* retired; `gaia:session` survives and its role carries the ceiling of what a
|
|
82
|
+
* machine identity may do.
|
|
83
|
+
*
|
|
84
|
+
* Zero was the original design, and a live control plane refuted it:
|
|
85
|
+
* `simple_oauth`'s Oauth2AccessPolicy rewrites a token account's permissions
|
|
86
|
+
* with `overwrite: TRUE`, and for a `client_credentials` token — every GAIA
|
|
87
|
+
* machine identity — the set becomes EXACTLY the scope's permissions, with
|
|
88
|
+
* neither user_role nor OG-rank permissions surviving. A request naming no scope
|
|
89
|
+
* against a consumer holding no default scopes does not even mint a token.
|
|
90
|
+
*/
|
|
91
|
+
export const OAUTH2_SCOPE = 'gaia:session';
|
|
92
|
+
/**
|
|
93
|
+
* Every `plugins[]` element that names {@link OAUTH2_PLUGIN_PACKAGE}, in order.
|
|
94
|
+
*
|
|
95
|
+
* Brace-balanced from the `{` that opens the element (the package name is the
|
|
96
|
+
* first key of every entry the scaffold and the migrations write) to its match,
|
|
97
|
+
* absorbing the trailing comma so an element can be removed whole. Comments are
|
|
98
|
+
* MASKED, not stripped, so these offsets index the real text.
|
|
99
|
+
*
|
|
100
|
+
* A `${…}` inside a template literal is brace-balanced too, which is why
|
|
101
|
+
* `token_url: \`${baseUrl}/oauth/token\`` needs no special case. An UNBALANCED
|
|
102
|
+
* file yields nothing at all rather than a guessed span — the migration then
|
|
103
|
+
* leaves it untouched, which is the same refusal `addRendererPluginEntry` makes
|
|
104
|
+
* for a config with no `plugins: [` literal.
|
|
105
|
+
*/
|
|
106
|
+
function oauth2EntrySpans(text) {
|
|
107
|
+
const probe = maskLineComments(text);
|
|
108
|
+
const spans = [];
|
|
109
|
+
let from = 0;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const hit = probe.indexOf(OAUTH2_PLUGIN_PACKAGE, from);
|
|
112
|
+
if (hit === -1)
|
|
113
|
+
return spans;
|
|
114
|
+
const start = probe.lastIndexOf('{', hit);
|
|
115
|
+
if (start === -1) {
|
|
116
|
+
from = hit + OAUTH2_PLUGIN_PACKAGE.length;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let i = start;
|
|
121
|
+
for (; i < probe.length; i += 1) {
|
|
122
|
+
if (probe[i] === '{')
|
|
123
|
+
depth += 1;
|
|
124
|
+
else if (probe[i] === '}') {
|
|
125
|
+
depth -= 1;
|
|
126
|
+
if (depth === 0) {
|
|
127
|
+
i += 1;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (depth !== 0)
|
|
133
|
+
return [];
|
|
134
|
+
let end = i;
|
|
135
|
+
while (probe[end] === ' ' || probe[end] === '\t')
|
|
136
|
+
end += 1;
|
|
137
|
+
if (probe[end] === ',')
|
|
138
|
+
end += 1;
|
|
139
|
+
spans.push({ start, end });
|
|
140
|
+
from = end;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Grow a span to its whole line, when the line holds nothing else. */
|
|
144
|
+
function wholeLineSpan(text, span) {
|
|
145
|
+
const lineStart = text.lastIndexOf('\n', span.start) + 1;
|
|
146
|
+
const start = text.slice(lineStart, span.start).trim() === '' ? lineStart : span.start;
|
|
147
|
+
const newline = text.indexOf('\n', span.end);
|
|
148
|
+
const end = newline !== -1 && text.slice(span.end, newline).trim() === ''
|
|
149
|
+
? newline + 1
|
|
150
|
+
: span.end;
|
|
151
|
+
return { start, end };
|
|
152
|
+
}
|
|
153
|
+
const SCOPE_VALUE = /(?:'[^']*'|"[^"]*")/.source;
|
|
154
|
+
/** Every `scope: '…'`, for rewriting the VALUE in place. */
|
|
155
|
+
const SCOPE_KEY_RE = new RegExp(`(\\bscope:\\s*)${SCOPE_VALUE}`, 'g');
|
|
156
|
+
/** Whether the element names a scope at all. Non-global: `.test` must not seek. */
|
|
157
|
+
const SCOPE_PRESENT_RE = new RegExp(`\\bscope:\\s*${SCOPE_VALUE}`);
|
|
158
|
+
const ID_LINE_RE = /^([ \t]*)id:\s*'[^']*',[ \t]*\r?\n/m;
|
|
159
|
+
const ID_INLINE_RE = /\bid:\s*'[^']*',/;
|
|
160
|
+
/** Whether the element names an id at all — no trailing comma required. */
|
|
161
|
+
const ID_PRESENT_RE = /\bid:\s*'[^']*'/;
|
|
162
|
+
/** `with: {` as the last thing on its line — the fallback insertion anchor. */
|
|
163
|
+
const WITH_LINE_RE = /^([ \t]*)with:\s*\{[ \t]*\r?\n/m;
|
|
164
|
+
/** `with: {` anywhere — the same anchor for a single-line `with: { … }`. */
|
|
165
|
+
const WITH_INLINE_RE = /\bwith:\s*\{/;
|
|
166
|
+
/**
|
|
167
|
+
* Ensure the element names {@link OAUTH2_PROVIDER_ID} as its provider id.
|
|
168
|
+
*
|
|
169
|
+
* The id is also the ANCHOR the two other v3 edits insert against, so resolving
|
|
170
|
+
* it first is what makes them reachable. An element that already names an id has
|
|
171
|
+
* that value rewritten; one that names none gets the key inserted at the head of
|
|
172
|
+
* its `with: { … }` block.
|
|
173
|
+
*
|
|
174
|
+
* An element with neither THROWS. That is the GAIA-391 round-2 correction: the
|
|
175
|
+
* first round used the `id:` key as its only anchor and simply returned such an
|
|
176
|
+
* element verbatim, so `gaia upgrade` re-stamped the file to v3 and reported
|
|
177
|
+
* success while leaving a provider dropsh cannot resolve — no id to select it by
|
|
178
|
+
* and no `default: true` to make it the fallback, i.e. a config that mints
|
|
179
|
+
* nothing. Silent success on a config that was not migrated is worse than a
|
|
180
|
+
* refusal the operator can act on, and the runner has already written the
|
|
181
|
+
* `.v<N>.bak` before this point, so the original is never lost.
|
|
182
|
+
*/
|
|
183
|
+
function ensureProviderId(entry) {
|
|
184
|
+
if (ID_PRESENT_RE.test(entry)) {
|
|
185
|
+
return entry.replace(/(\bid:\s*)'[^']*'/, `$1'${OAUTH2_PROVIDER_ID}'`);
|
|
186
|
+
}
|
|
187
|
+
const lineForm = entry.replace(WITH_LINE_RE, (line, indent) => `${line}${indent} id: '${OAUTH2_PROVIDER_ID}',\n`);
|
|
188
|
+
if (lineForm !== entry)
|
|
189
|
+
return lineForm;
|
|
190
|
+
const inlineForm = entry.replace(WITH_INLINE_RE, (open) => `${open} id: '${OAUTH2_PROVIDER_ID}',`);
|
|
191
|
+
if (inlineForm !== entry)
|
|
192
|
+
return inlineForm;
|
|
193
|
+
throw new Error(`cannot migrate a ${OAUTH2_PLUGIN_PACKAGE} entry that names neither an ` +
|
|
194
|
+
`\`id:\` key nor a \`with: { … }\` block: there is nowhere to put the ` +
|
|
195
|
+
`single '${OAUTH2_PROVIDER_ID}' identity. Give the entry a ` +
|
|
196
|
+
`\`with: { id: '${OAUTH2_PROVIDER_ID}', scope: '${OAUTH2_SCOPE}', … }\` ` +
|
|
197
|
+
`block by hand, then re-run \`gaia upgrade\`.`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Rewrite ONE oauth2 element into the single `gaia` provider.
|
|
201
|
+
*
|
|
202
|
+
* Three edits, each idempotent on already-collapsed text: the id becomes
|
|
203
|
+
* {@link OAUTH2_PROVIDER_ID} (inserted when the element names none — see
|
|
204
|
+
* {@link ensureProviderId}, which refuses loudly when it cannot be placed); the
|
|
205
|
+
* scope is normalised to the one surviving scope {@link OAUTH2_SCOPE}, and ADDED
|
|
206
|
+
* when the element names none; and `default: true` is asserted, because with one
|
|
207
|
+
* provider the resolver must be able to pick it with no profile argument.
|
|
208
|
+
*
|
|
209
|
+
* The scope is kept rather than deleted, and that is the correction GAIA-391
|
|
210
|
+
* measured rather than assumed. `simple_oauth`'s Oauth2AccessPolicy rewrites a
|
|
211
|
+
* token account's permissions with `overwrite: TRUE`, and for a
|
|
212
|
+
* `client_credentials` token the result is EXACTLY the scope's permissions — so
|
|
213
|
+
* a scopeless config either fails to mint at all (`invalid_request`, hint
|
|
214
|
+
* `Check the scope parameter`) or silently inherits whatever the consumer's
|
|
215
|
+
* default scopes happen to be. The rank still decides WHICH rows are reachable;
|
|
216
|
+
* the scope decides which operations exist at all.
|
|
217
|
+
*/
|
|
218
|
+
function collapseOauth2Entry(entry) {
|
|
219
|
+
let out = ensureProviderId(entry);
|
|
220
|
+
if (SCOPE_PRESENT_RE.test(out)) {
|
|
221
|
+
out = out.replace(SCOPE_KEY_RE, `$1'${OAUTH2_SCOPE}'`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const lineForm = out.replace(ID_LINE_RE, (line, indent) => `${line}${indent}scope: '${OAUTH2_SCOPE}',\n`);
|
|
225
|
+
out =
|
|
226
|
+
lineForm !== out
|
|
227
|
+
? lineForm
|
|
228
|
+
: out.replace(ID_INLINE_RE, (id) => `${id} scope: '${OAUTH2_SCOPE}',`);
|
|
229
|
+
}
|
|
230
|
+
if (/\bdefault:\s*true\b/.test(out))
|
|
231
|
+
return out;
|
|
232
|
+
const lineForm = out.replace(ID_LINE_RE, (line, indent) => `${line}${indent}default: true,\n`);
|
|
233
|
+
if (lineForm !== out)
|
|
234
|
+
return lineForm;
|
|
235
|
+
return out.replace(ID_INLINE_RE, (id) => `${id} default: true,`);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* GAIA-391 (v2 → v3): collapse a connection's oauth2 entries into ONE `gaia`
|
|
239
|
+
* provider on ONE scope — the whole of the one-identity change, as a pure text
|
|
240
|
+
* transform.
|
|
241
|
+
*
|
|
242
|
+
* A MIGRATION rather than a cutover (spec D3): operators have hand-edited these
|
|
243
|
+
* files — a pinned literal `baseUrl`, a per-install `workspace_id`, an extra
|
|
244
|
+
* `addons[]` entry — so every value outside the two elements being merged is
|
|
245
|
+
* carried over verbatim, and the runner never EXECUTES a possibly
|
|
246
|
+
* side-effecting config module.
|
|
247
|
+
*
|
|
248
|
+
* The FIRST oauth2 element survives (it is the one v2 marked `default: true`);
|
|
249
|
+
* the rest are removed line-and-all. A config with no oauth2 element is left
|
|
250
|
+
* exactly as it is — that is the one silent no-op, and it is silent because
|
|
251
|
+
* there is nothing to collapse. An element that IS there but cannot be given the
|
|
252
|
+
* `gaia` identity throws (see {@link ensureProviderId}) rather than passing
|
|
253
|
+
* through unchanged.
|
|
254
|
+
*/
|
|
255
|
+
export function collapseOauth2Providers(text) {
|
|
256
|
+
const spans = oauth2EntrySpans(text);
|
|
257
|
+
const first = spans[0];
|
|
258
|
+
if (first === undefined)
|
|
259
|
+
return text;
|
|
260
|
+
let out = text;
|
|
261
|
+
// From the END, so the surviving element's offsets stay valid.
|
|
262
|
+
for (const span of spans.slice(1).reverse()) {
|
|
263
|
+
const line = wholeLineSpan(out, span);
|
|
264
|
+
out = out.slice(0, line.start) + out.slice(line.end);
|
|
265
|
+
}
|
|
266
|
+
return (out.slice(0, first.start) +
|
|
267
|
+
collapseOauth2Entry(out.slice(first.start, first.end)) +
|
|
268
|
+
out.slice(first.end));
|
|
269
|
+
}
|
|
52
270
|
/**
|
|
53
271
|
* The ordered, contiguous migration chain. v1 was the pre-GAIA-226 shape (auth
|
|
54
|
-
* plugins only); v2 adds the markdown renderer entry
|
|
55
|
-
*
|
|
56
|
-
*
|
|
272
|
+
* plugins only); v2 adds the markdown renderer entry; v3 collapses the two
|
|
273
|
+
* differently-scoped oauth2 providers into ONE identity on ONE scope (GAIA-391)
|
|
274
|
+
* — one, not zero: see {@link OAUTH2_SCOPE} for why the scope survives.
|
|
275
|
+
* Nothing below v1 is a versioned config (v0 = seed / hand-authored / untouched).
|
|
276
|
+
* `GAIA_CONFIG_SCHEMA_VERSION` derives from the chain's last `to` — never
|
|
277
|
+
* hardcode it.
|
|
57
278
|
*/
|
|
58
279
|
export const CONNECTION_MIGRATIONS = [
|
|
59
280
|
{
|
|
@@ -62,6 +283,13 @@ export const CONNECTION_MIGRATIONS = [
|
|
|
62
283
|
description: `add the ${RENDERER_PLUGIN_PACKAGE} renderer entry to plugins[]`,
|
|
63
284
|
apply: addRendererPluginEntry,
|
|
64
285
|
},
|
|
286
|
+
{
|
|
287
|
+
from: 2,
|
|
288
|
+
to: 3,
|
|
289
|
+
description: `collapse the two ${OAUTH2_PLUGIN_PACKAGE} entries into one ` +
|
|
290
|
+
`'${OAUTH2_PROVIDER_ID}' provider on scope '${OAUTH2_SCOPE}'`,
|
|
291
|
+
apply: collapseOauth2Providers,
|
|
292
|
+
},
|
|
65
293
|
];
|
|
66
294
|
/** The current connection-config schema version — single source of truth. */
|
|
67
295
|
export const GAIA_CONFIG_SCHEMA_VERSION = CONNECTION_MIGRATIONS.at(-1)?.to ?? 1;
|
package/dist/src/cli/init.d.ts
CHANGED
|
@@ -50,8 +50,10 @@ export declare function renderCommittedConfig(inputs: Pick<InitInputs, 'project'
|
|
|
50
50
|
* The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
|
|
51
51
|
* dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
|
|
52
52
|
* conductor's auth. Reads base_url + client credentials from the user-global
|
|
53
|
-
* machine context (`~/.gaia/machine.config.js`) and declares the
|
|
54
|
-
*
|
|
53
|
+
* machine context (`~/.gaia/machine.config.js`) and declares the ONE default
|
|
54
|
+
* oauth2 provider, `gaia`, on the one scope `gaia:session` (GAIA-391). The
|
|
55
|
+
* scope is the capability ceiling for a client_credentials token; the rank an
|
|
56
|
+
* identity holds in a workspace decides which rows it reaches.
|
|
55
57
|
*/
|
|
56
58
|
export declare function renderGaiaConfig(): string;
|
|
57
59
|
/** The user-global machine context module: identity + connection (incl. secret). */
|
package/dist/src/cli/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { machineContextPath, readMachineContext, } from '@gaia-ai/core';
|
|
5
|
-
import { GAIA_CONFIG_SCHEMA_VERSION, RENDERER_PLUGIN_ENTRY, } from './config-schema.js';
|
|
5
|
+
import { GAIA_CONFIG_SCHEMA_VERSION, OAUTH2_PROVIDER_ID, OAUTH2_SCOPE, RENDERER_PLUGIN_ENTRY, } from './config-schema.js';
|
|
6
6
|
export { machineContextPath, readMachineContext };
|
|
7
7
|
/** JS single-quoted string literal for a trusted, simple value. */
|
|
8
8
|
function q(value) {
|
|
@@ -56,16 +56,17 @@ async function loadLocal() {
|
|
|
56
56
|
const machine = await loadMachine();
|
|
57
57
|
const local = await loadLocal();
|
|
58
58
|
const project = local.project ?? ${q(inputs.project)};
|
|
59
|
-
const
|
|
59
|
+
const composedConductorId =
|
|
60
60
|
machine.user_id && machine.machine_id
|
|
61
61
|
? \`\${machine.user_id}-\${machine.machine_id}-\${project}\`
|
|
62
62
|
: undefined;
|
|
63
63
|
|
|
64
64
|
export default {
|
|
65
65
|
project,
|
|
66
|
-
machine_id
|
|
66
|
+
// Registration key (GAIA-353 renamed from machine_id). Host capacity lives on
|
|
67
|
+
// machine.config.js as max_parallel — do not set max_parallel here.
|
|
68
|
+
conductor_id: local.conductor_id ?? local.machine_id ?? composedConductorId,
|
|
67
69
|
states: ['spec', 'diagnose', 'coding', 'review'],
|
|
68
|
-
max_parallel: 5,
|
|
69
70
|
// Lifecycle hooks are executor-owned (GAIA-84): they live at the config top
|
|
70
71
|
// level — NOT on a plugin descriptor's \`with.hooks\`.
|
|
71
72
|
hooks: { after_create: 'ddev init-worktree', after_done: 'ddev delete -Oy' },
|
|
@@ -88,8 +89,10 @@ export default {
|
|
|
88
89
|
* The committed CONNECTION config `./.gaia/gaia.config.js` (GAIA-201). A
|
|
89
90
|
* dropsh-shaped `{ site, plugins }` read by `gaia ui`, `gaia dropsh`, and the
|
|
90
91
|
* conductor's auth. Reads base_url + client credentials from the user-global
|
|
91
|
-
* machine context (`~/.gaia/machine.config.js`) and declares the
|
|
92
|
-
*
|
|
92
|
+
* machine context (`~/.gaia/machine.config.js`) and declares the ONE default
|
|
93
|
+
* oauth2 provider, `gaia`, on the one scope `gaia:session` (GAIA-391). The
|
|
94
|
+
* scope is the capability ceiling for a client_credentials token; the rank an
|
|
95
|
+
* identity holds in a workspace decides which rows it reaches.
|
|
93
96
|
*/
|
|
94
97
|
export function renderGaiaConfig() {
|
|
95
98
|
return `// @gaia-schema-version ${GAIA_CONFIG_SCHEMA_VERSION}
|
|
@@ -121,37 +124,45 @@ const local = await loadLocal();
|
|
|
121
124
|
const baseUrl = local.base_url ?? machine.base_url;
|
|
122
125
|
const clientId = local.oauth?.client_id ?? machine.client_id ?? 'gaia-agent';
|
|
123
126
|
const clientSecret = local.oauth?.client_secret ?? machine.client_secret;
|
|
127
|
+
// GAIA-351: the ambient workspace every create is bound to. REQUIRED — see the
|
|
128
|
+
// top-level \`workspace_id\` below.
|
|
129
|
+
const workspaceId = local.workspace_id ?? machine.workspace_id;
|
|
124
130
|
|
|
125
131
|
export default {
|
|
126
132
|
// GAIA-216: the connection-config schema version — kept in sync with the
|
|
127
133
|
// header marker above so \`gaia upgrade\` can migrate a stale-shape config.
|
|
128
134
|
schema_version: ${GAIA_CONFIG_SCHEMA_VERSION},
|
|
129
135
|
site: { base_url: baseUrl, jsonapi_prefix: local.jsonapi_prefix ?? '/jsonapi' },
|
|
136
|
+
// GAIA-351: REQUIRED, and top-level — a sibling of \`site\`, never a member of
|
|
137
|
+
// it and never an addon's \`with\`. The filler that consumes it ships inside
|
|
138
|
+
// \`@gaia-ai/addon-essentials\`, which takes no options, so this is the only
|
|
139
|
+
// place a connection ever names its workspace. \`loadGaiaConfig\` refuses a
|
|
140
|
+
// config without it, so set \`workspace_id\` in ~/.gaia/machine.config.js (or
|
|
141
|
+
// this repo's conductor.config.local.js) to the uuid of the gaia_workspace
|
|
142
|
+
// this checkout creates into.
|
|
143
|
+
workspace_id: workspaceId,
|
|
130
144
|
plugins: [
|
|
131
145
|
${RENDERER_PLUGIN_ENTRY}
|
|
146
|
+
// GAIA-391: ONE identity, on ONE scope. There used to be two entries here
|
|
147
|
+
// that differed only by the scope they requested, and every write had to
|
|
148
|
+
// pick one — that choice is what went away. The id names the CONNECTION,
|
|
149
|
+
// not a capability, which is why it is not named after a job.
|
|
150
|
+
//
|
|
151
|
+
// The scope is not ceremony: for a client_credentials token simple_oauth
|
|
152
|
+
// overwrites the account's permissions with EXACTLY the scope's, so this is
|
|
153
|
+
// the ceiling of what this identity may do. The rank it holds in a workspace
|
|
154
|
+
// still decides WHICH rows it reaches.
|
|
132
155
|
{
|
|
133
156
|
plugin: '@dropsh/plugin-oauth2',
|
|
134
157
|
export: 'oauth2Plugin',
|
|
135
158
|
with: {
|
|
136
|
-
id: '
|
|
159
|
+
id: '${OAUTH2_PROVIDER_ID}',
|
|
137
160
|
default: true,
|
|
138
161
|
type: 'oauth2_client_credentials',
|
|
139
162
|
client_id: clientId,
|
|
140
163
|
client_secret: clientSecret,
|
|
141
164
|
token_url: \`\${baseUrl}/oauth/token\`,
|
|
142
|
-
scope: '
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
plugin: '@dropsh/plugin-oauth2',
|
|
147
|
-
export: 'oauth2Plugin',
|
|
148
|
-
with: {
|
|
149
|
-
id: 'pm',
|
|
150
|
-
type: 'oauth2_client_credentials',
|
|
151
|
-
client_id: clientId,
|
|
152
|
-
client_secret: clientSecret,
|
|
153
|
-
token_url: \`\${baseUrl}/oauth/token\`,
|
|
154
|
-
scope: 'gaia:project_manager',
|
|
165
|
+
scope: '${OAUTH2_SCOPE}',
|
|
155
166
|
},
|
|
156
167
|
},
|
|
157
168
|
],
|
|
@@ -160,18 +171,22 @@ export default {
|
|
|
160
171
|
}
|
|
161
172
|
/** The user-global machine context module: identity + connection (incl. secret). */
|
|
162
173
|
export function renderMachineContext(ctx) {
|
|
174
|
+
const maxParallel = ctx.max_parallel ?? 1;
|
|
163
175
|
return `// User-global GAIA machine context — gitignored, user-only (chmod 0600), never
|
|
164
176
|
// committed. A plain importable module holding your machine identity +
|
|
165
177
|
// connection, incl. the OAuth client secret. The engine conductor.config.js
|
|
166
|
-
// composes
|
|
167
|
-
// connection gaia.config.js reads base_url / client_id / client_secret.
|
|
168
|
-
//
|
|
178
|
+
// composes conductor_id (\`\${user_id}-\${machine_id}-\${project}\`) from it; the
|
|
179
|
+
// connection gaia.config.js reads base_url / client_id / client_secret.
|
|
180
|
+
// max_parallel is the host concurrent-run budget (GAIA-353) — not an engine
|
|
181
|
+
// config field. Created and gap-filled by \`gaia conductor init\`; existing
|
|
182
|
+
// values are never overwritten.
|
|
169
183
|
export default {
|
|
170
184
|
machine_id: ${q(ctx.machine_id)},
|
|
171
185
|
user_id: ${q(ctx.user_id)},
|
|
172
186
|
base_url: ${q(ctx.base_url)},
|
|
173
187
|
client_id: ${q(ctx.client_id)},
|
|
174
188
|
client_secret: ${q(ctx.client_secret)},
|
|
189
|
+
max_parallel: ${maxParallel},
|
|
175
190
|
};
|
|
176
191
|
`;
|
|
177
192
|
}
|
|
@@ -189,6 +204,7 @@ export async function scaffoldMachineContext(opts) {
|
|
|
189
204
|
base_url: opts.baseUrl,
|
|
190
205
|
client_id: opts.clientId,
|
|
191
206
|
client_secret: opts.secret,
|
|
207
|
+
max_parallel: 1,
|
|
192
208
|
};
|
|
193
209
|
const filledKeys = [];
|
|
194
210
|
const merged = { ...derived, ...existing };
|
|
@@ -205,6 +221,12 @@ export async function scaffoldMachineContext(opts) {
|
|
|
205
221
|
filledKeys.push(key);
|
|
206
222
|
}
|
|
207
223
|
}
|
|
224
|
+
if (typeof existing.max_parallel !== 'number' ||
|
|
225
|
+
!Number.isFinite(existing.max_parallel) ||
|
|
226
|
+
(existing.max_parallel ?? 0) < 1) {
|
|
227
|
+
merged.max_parallel = 1;
|
|
228
|
+
filledKeys.push('max_parallel');
|
|
229
|
+
}
|
|
208
230
|
const created = !existsSync(opts.path);
|
|
209
231
|
if (filledKeys.length > 0 || created) {
|
|
210
232
|
mkdirSync(dirname(opts.path), { recursive: true });
|
|
@@ -4,6 +4,7 @@ import { createInterface } from 'node:readline';
|
|
|
4
4
|
import { CommandRunner, createLogger, deriveConductorLiveness, exec, fetchUpdateNotice, loadGaiaConfig, machineContextPath, printVersionLine, readMachineContext, resolveConfigPath, setDefaultCommandRunner, signalVerifiedProcess, } from '@gaia-ai/core';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { authStatus } from 'dropsh';
|
|
7
|
+
import { OAUTH2_PROVIDER_ID } from '../cli/config-schema.js';
|
|
7
8
|
import { scaffold } from '../cli/init.js';
|
|
8
9
|
import { runUpgrade } from '../cli/upgrade.js';
|
|
9
10
|
import { composeConductorConfig, loadConductorConfig } from '../config.js';
|
|
@@ -36,14 +37,14 @@ function checkoutRootOf(config) {
|
|
|
36
37
|
return dirname(config.config_path);
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* The conductor's stable identity (gaia_conductor.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* The conductor's stable identity (gaia_conductor.conductor_id). Required —
|
|
41
|
+
* the loader throws without it — so lifecycle commands read it here, never
|
|
42
|
+
* re-derive. Soft-reads legacy machine_id during the GAIA-353 cutover.
|
|
42
43
|
*/
|
|
43
44
|
function conductorIdOf(config) {
|
|
44
|
-
const id = config.machine_id;
|
|
45
|
+
const id = config.conductor_id ?? config.machine_id;
|
|
45
46
|
if (id === undefined || id.trim() === '') {
|
|
46
|
-
throw new Error('conductor config has no
|
|
47
|
+
throw new Error('conductor config has no conductor_id');
|
|
47
48
|
}
|
|
48
49
|
return id;
|
|
49
50
|
}
|
|
@@ -575,10 +576,15 @@ function registerInit(conductor) {
|
|
|
575
576
|
? `updated ${m.path} (filled: ${m.filledKeys.join(', ')})`
|
|
576
577
|
: `kept ${m.path} (already complete)`);
|
|
577
578
|
}
|
|
579
|
+
// GAIA-391: ONE login, because the scaffolded connection declares ONE
|
|
580
|
+
// oauth2 provider. The id is interpolated from OAUTH2_PROVIDER_ID — the
|
|
581
|
+
// same constant `renderGaiaConfig` and the v2→v3 migration emit — so
|
|
582
|
+
// this first-impression block can never name a provider the config does
|
|
583
|
+
// not have. It used to print two logins and `# both profiles present`;
|
|
584
|
+
// neither id resolved and that status line could not become true.
|
|
578
585
|
console.log('\nNext steps:\n' +
|
|
579
|
-
|
|
580
|
-
' gaia dropsh auth
|
|
581
|
-
' gaia dropsh auth status # both profiles present');
|
|
586
|
+
` gaia dropsh auth login --provider ${OAUTH2_PROVIDER_ID}\n` +
|
|
587
|
+
' gaia dropsh auth status');
|
|
582
588
|
});
|
|
583
589
|
}
|
|
584
590
|
/** The `conductor` command plugin the host mounts (GAIA-201). GAIA-224
|
package/dist/src/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { discoverAddons, emptyContributions, findDefaultExportObject, matchDelimiter, resolveConductorSlots, resolveModuleEslintStyle, scanTopLevelProperties, } from '@gaia-ai/core';
|
|
4
|
+
import { discoverAddons, emptyContributions, findDefaultExportObject, matchDelimiter, readMachineContext, resolveConductorSlots, resolveModuleEslintStyle, scanTopLevelProperties, } from '@gaia-ai/core';
|
|
5
5
|
import { narrowConductorContributions } from './plugins/preset.js';
|
|
6
6
|
// GAIA-201: the `.gaia/` walk-up + connection resolution moved to `@gaia-ai/core`
|
|
7
7
|
// (`resolveConfigPath` / `resolveGaiaConfigPath` / `findGaiaDir`). Re-export the
|
|
@@ -187,18 +187,45 @@ export async function loadConductorConfig(configFile) {
|
|
|
187
187
|
const states = Array.isArray(config.states)
|
|
188
188
|
? config.states.map((state) => requireNonEmptyString(state, 'states'))
|
|
189
189
|
: [];
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
// conductor_id is required (GAIA-353 renamed from machine_id). Prefer the new
|
|
191
|
+
// key; accept legacy machine_id with a one-shot warning. No derived fallback.
|
|
192
|
+
// A repo's committed config composes it from the machine context
|
|
193
|
+
// (`${user_id}-${host}-${project}`); co-located configs each compose their
|
|
194
|
+
// own from their project.
|
|
195
|
+
let conductorId;
|
|
196
|
+
if (typeof config.conductor_id === 'string' &&
|
|
197
|
+
config.conductor_id.trim() !== '') {
|
|
198
|
+
conductorId = config.conductor_id.trim();
|
|
199
|
+
}
|
|
200
|
+
else if (typeof config.machine_id === 'string' &&
|
|
201
|
+
config.machine_id.trim() !== '') {
|
|
202
|
+
console.warn(`[gaia] ${configPath}: engine key "machine_id" is deprecated; use "conductor_id" (GAIA-353)`);
|
|
203
|
+
conductorId = config.machine_id.trim();
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
throw new Error(`conductor config requires non-empty conductor_id (legacy machine_id accepted)`);
|
|
207
|
+
}
|
|
199
208
|
const label = typeof config.label === 'string' && config.label.trim() !== ''
|
|
200
209
|
? config.label
|
|
201
|
-
:
|
|
210
|
+
: conductorId;
|
|
211
|
+
// Capacity SoT is the machine context (GAIA-353). Engine max_parallel is
|
|
212
|
+
// ignored with a warning so leftover configs do not reintroduce a per-process
|
|
213
|
+
// private quota.
|
|
214
|
+
if (config.max_parallel !== undefined) {
|
|
215
|
+
console.warn(`[gaia] ${configPath}: engine "max_parallel" is ignored; set max_parallel on ~/.gaia/machine.config.js (GAIA-353)`);
|
|
216
|
+
}
|
|
217
|
+
const machineCtx = await readMachineContext();
|
|
218
|
+
const maxParallel = typeof machineCtx.max_parallel === 'number' &&
|
|
219
|
+
Number.isFinite(machineCtx.max_parallel) &&
|
|
220
|
+
machineCtx.max_parallel >= 1
|
|
221
|
+
? Math.floor(machineCtx.max_parallel)
|
|
222
|
+
: 1;
|
|
223
|
+
// Host stem for gaia_machine / registration (GAIA-353) — prefer the machine
|
|
224
|
+
// context id over hyphen-splitting the composed conductor_id.
|
|
225
|
+
const hostKey = typeof machineCtx.machine_id === 'string' &&
|
|
226
|
+
machineCtx.machine_id.trim() !== ''
|
|
227
|
+
? machineCtx.machine_id.trim()
|
|
228
|
+
: undefined;
|
|
202
229
|
// GAIA-215: discover the conductor `addons: []` surface (last-wins singletons,
|
|
203
230
|
// agent candidate list). A pre-existing per-slot descriptor still loads and
|
|
204
231
|
// WINS over a discovered contributor of the same kind (back-compat additive).
|
|
@@ -245,13 +272,16 @@ export async function loadConductorConfig(configFile) {
|
|
|
245
272
|
agent,
|
|
246
273
|
workspace,
|
|
247
274
|
label,
|
|
248
|
-
|
|
275
|
+
conductor_id: conductorId,
|
|
276
|
+
// Soft alias so callers that still read machine_id keep working this cut.
|
|
277
|
+
machine_id: conductorId,
|
|
249
278
|
project,
|
|
250
279
|
states,
|
|
251
280
|
prompt: typeof config.prompt === 'string' && config.prompt.trim() !== ''
|
|
252
281
|
? config.prompt
|
|
253
282
|
: DEFAULT_AGENT_PROMPT,
|
|
254
|
-
max_parallel:
|
|
283
|
+
max_parallel: maxParallel,
|
|
284
|
+
...(hostKey !== undefined ? { host_key: hostKey } : {}),
|
|
255
285
|
poll_interval_ms: optionalPositiveInteger(config.poll_interval_ms, 5000, 'poll_interval_ms'),
|
|
256
286
|
lease_seconds: optionalPositiveInteger(config.lease_seconds, 300, 'lease_seconds'),
|
|
257
287
|
...(config.hooks !== undefined ? { hooks: config.hooks } : {}),
|
|
@@ -25,6 +25,12 @@ export declare class Conductor {
|
|
|
25
25
|
private uuid;
|
|
26
26
|
constructor(config: ConductorFileConfig, remote: GaiaRemote, executor: GaiaExecutor, workspace: GaiaWorkspace, agents: ResolvedAgent[], logger: ConductorLogger, checkoutRoot?: string);
|
|
27
27
|
get id(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Host stem for gaia_machine.host_key. Prefer config.host_key from
|
|
30
|
+
* MachineContext.machine_id; fall back to a best-effort parse of the composed
|
|
31
|
+
* conductor_id only when the loader could not supply the stem.
|
|
32
|
+
*/
|
|
33
|
+
private get hostKey();
|
|
28
34
|
/**
|
|
29
35
|
* Whether this instance is the serve-loop process itself (GAIA-232).
|
|
30
36
|
*
|
|
@@ -103,7 +103,27 @@ export class Conductor {
|
|
|
103
103
|
this.checkoutRoot = checkoutRoot;
|
|
104
104
|
}
|
|
105
105
|
get id() {
|
|
106
|
-
return this.config.
|
|
106
|
+
return (this.config.conductor_id ??
|
|
107
|
+
this.config.machine_id ??
|
|
108
|
+
conductorId(this.checkoutRoot));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Host stem for gaia_machine.host_key. Prefer config.host_key from
|
|
112
|
+
* MachineContext.machine_id; fall back to a best-effort parse of the composed
|
|
113
|
+
* conductor_id only when the loader could not supply the stem.
|
|
114
|
+
*/
|
|
115
|
+
get hostKey() {
|
|
116
|
+
const fromConfig = this.config.host_key?.trim();
|
|
117
|
+
if (fromConfig)
|
|
118
|
+
return fromConfig;
|
|
119
|
+
const id = this.id;
|
|
120
|
+
const parts = id.split('-');
|
|
121
|
+
if (parts.length >= 3 && parts[0] && parts[1]) {
|
|
122
|
+
const middle = parts.slice(1, -1).join('-');
|
|
123
|
+
if (middle)
|
|
124
|
+
return middle;
|
|
125
|
+
}
|
|
126
|
+
return id;
|
|
107
127
|
}
|
|
108
128
|
/**
|
|
109
129
|
* Whether this instance is the serve-loop process itself (GAIA-232).
|
|
@@ -130,6 +150,7 @@ export class Conductor {
|
|
|
130
150
|
registration() {
|
|
131
151
|
const base = {
|
|
132
152
|
id: this.id,
|
|
153
|
+
host_key: this.hostKey,
|
|
133
154
|
project: this.config.project,
|
|
134
155
|
states: this.config.states,
|
|
135
156
|
workspace: this.checkoutRoot,
|
|
@@ -163,15 +184,28 @@ export class Conductor {
|
|
|
163
184
|
if (!this.uuid)
|
|
164
185
|
throw new Error('not started');
|
|
165
186
|
let count = await this.remote.activeRunCount(this.id);
|
|
166
|
-
// A heartbeat is the server-side self-heal: it upserts by
|
|
167
|
-
// never 404s and recreates a vanished registration) and refreshes the
|
|
168
|
-
// — no client-side re-register, no separate status read.
|
|
187
|
+
// A heartbeat is the server-side self-heal: it upserts by conductor_id (so
|
|
188
|
+
// it never 404s and recreates a vanished registration) and refreshes the
|
|
189
|
+
// lease — no client-side re-register, no separate status read. It also
|
|
190
|
+
// recomputes gaia_machine.current_load (peers + this process's load).
|
|
169
191
|
await this.remote.heartbeat(this.registration(), count, this.config.lease_seconds);
|
|
170
|
-
|
|
192
|
+
// GAIA-353 D5: gate claims on the shared host budget, not only this
|
|
193
|
+
// process's active runs. After the heartbeat above, machine.current_load
|
|
194
|
+
// includes peers + self; each claim below increments the local view so
|
|
195
|
+
// pending dispatches count without a second round-trip.
|
|
196
|
+
const capacity = await this.remote.machineCapacity(this.hostKey);
|
|
197
|
+
let machineLoad = capacity?.currentLoad ?? count;
|
|
198
|
+
const maxParallel = this.config.max_parallel;
|
|
199
|
+
this.logger.info({
|
|
200
|
+
conductorId: this.id,
|
|
201
|
+
active: count,
|
|
202
|
+
machineLoad,
|
|
203
|
+
capacity: maxParallel,
|
|
204
|
+
}, 'conductor tick');
|
|
171
205
|
await this.finalizeDoneRuns();
|
|
172
206
|
await this.reap();
|
|
173
207
|
let claimed = 0;
|
|
174
|
-
while (
|
|
208
|
+
while (machineLoad < maxParallel) {
|
|
175
209
|
const run = await this.remote.claimNext({
|
|
176
210
|
leaseSeconds: this.config.lease_seconds,
|
|
177
211
|
conductorId: this.id,
|
|
@@ -203,14 +237,16 @@ export class Conductor {
|
|
|
203
237
|
}
|
|
204
238
|
}
|
|
205
239
|
count += 1;
|
|
240
|
+
machineLoad += 1;
|
|
206
241
|
claimed += 1;
|
|
207
242
|
}
|
|
208
243
|
if (claimed === 0) {
|
|
209
244
|
this.logger.info({
|
|
210
245
|
conductorId: this.id,
|
|
211
246
|
active: count,
|
|
212
|
-
|
|
213
|
-
|
|
247
|
+
machineLoad,
|
|
248
|
+
capacity: maxParallel,
|
|
249
|
+
}, machineLoad >= maxParallel
|
|
214
250
|
? 'at capacity, nothing claimed'
|
|
215
251
|
: 'idle, nothing to claim');
|
|
216
252
|
}
|
|
@@ -533,6 +569,7 @@ export class Conductor {
|
|
|
533
569
|
await this.workspace.applyOpenLayout?.(ws.path, {
|
|
534
570
|
identifier: t.identifier,
|
|
535
571
|
branch: t.branchName,
|
|
572
|
+
title: t.title,
|
|
536
573
|
});
|
|
537
574
|
}
|
|
538
575
|
await this.executor.runHook('before_run', ws.path, { ticket: t.identifier }, env);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export interface ConductorRegistration {
|
|
2
|
+
/** Conductor process registration key (gaia_conductor.conductor_id). */
|
|
2
3
|
id: string;
|
|
4
|
+
/** Host identity stem (gaia_machine.host_key / MachineContext.machine_id). */
|
|
5
|
+
host_key?: string;
|
|
3
6
|
project: string;
|
|
4
7
|
/** Empty = serve all claimable states in the project (GAIA-207). */
|
|
5
8
|
states: string[];
|
|
@@ -163,6 +166,16 @@ export interface GaiaRemote {
|
|
|
163
166
|
setConductorStatus(conductorId: string, status: 'offline' | 'online'): Promise<void>;
|
|
164
167
|
listConductors(owner?: 'me'): Promise<ConductorStatus[]>;
|
|
165
168
|
activeRunCount(conductorId: string): Promise<number>;
|
|
169
|
+
/**
|
|
170
|
+
* Shared host capacity for this `host_key` (gaia_machine, GAIA-353 D5).
|
|
171
|
+
* After a heartbeat that reported this process's load, `currentLoad` is the
|
|
172
|
+
* sum of online conductors on the machine (peers + self). Null when the
|
|
173
|
+
* control plane has no machine row yet — callers fall back to local count.
|
|
174
|
+
*/
|
|
175
|
+
machineCapacity(hostKey: string): Promise<{
|
|
176
|
+
maxParallel: number;
|
|
177
|
+
currentLoad: number;
|
|
178
|
+
} | null>;
|
|
166
179
|
fetchActiveRuns(conductorId: string): Promise<ActiveRun[]>;
|
|
167
180
|
claimNext(claim: ClaimOptions): Promise<ClaimedRun | null>;
|
|
168
181
|
getTicket(ticketUuid: string): Promise<Ticket>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -5,10 +5,14 @@ export interface ConductorSettings {
|
|
|
5
5
|
/** Default: `${project} @ ${checkoutRoot}`. */
|
|
6
6
|
label: string;
|
|
7
7
|
/**
|
|
8
|
-
* Stable
|
|
9
|
-
*
|
|
10
|
-
* a known identity — e.g. so a ticket can be pre-assigned to
|
|
11
|
-
* assignment is the run-start trigger)
|
|
8
|
+
* Stable conductor registration key (gaia_conductor.conductor_id). Composed
|
|
9
|
+
* as `${user_id}-${host}-${project}` from the machine context. Set it to pin
|
|
10
|
+
* a conductor to a known identity — e.g. so a ticket can be pre-assigned to
|
|
11
|
+
* it (conductor assignment is the run-start trigger).
|
|
12
|
+
*/
|
|
13
|
+
conductor_id?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated GAIA-353 — alias for `conductor_id` during cutover.
|
|
12
16
|
*/
|
|
13
17
|
machine_id?: string;
|
|
14
18
|
/** Project name (gaia_project.name); resolved at registration. */
|
|
@@ -26,8 +30,17 @@ export interface ConductorSettings {
|
|
|
26
30
|
* `{state}`, `{runUuid}`. Defaults to `DEFAULT_AGENT_PROMPT`.
|
|
27
31
|
*/
|
|
28
32
|
prompt: string;
|
|
29
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Host concurrent-run budget. Sourced from machine context max_parallel
|
|
35
|
+
* (GAIA-353); engine-config leftovers are ignored with a warning.
|
|
36
|
+
*/
|
|
30
37
|
max_parallel: number;
|
|
38
|
+
/**
|
|
39
|
+
* Host identity stem (gaia_machine.host_key). Sourced from
|
|
40
|
+
* MachineContext.machine_id — not derived by hyphen-splitting conductor_id
|
|
41
|
+
* (that mis-parses when project or host segments contain hyphens).
|
|
42
|
+
*/
|
|
43
|
+
host_key?: string;
|
|
31
44
|
poll_interval_ms: number;
|
|
32
45
|
lease_seconds: number;
|
|
33
46
|
hooks?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "GAIA conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"directory": "gaia-cli/conductor"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@gaia-ai/core": "^0.
|
|
32
|
-
"@dropsh/plugin-oauth2": "^0.
|
|
33
|
-
"@dropsh/plugin-jsonapi-schema": "^0.
|
|
31
|
+
"@gaia-ai/core": "^0.11.0",
|
|
32
|
+
"@dropsh/plugin-oauth2": "^0.6.1",
|
|
33
|
+
"@dropsh/plugin-jsonapi-schema": "^0.6.1",
|
|
34
34
|
"commander": "^12.1.0",
|
|
35
|
-
"dropsh": "^0.
|
|
35
|
+
"dropsh": "^0.6.1"
|
|
36
36
|
}
|
|
37
37
|
}
|