orizu 0.5.44 → 0.5.45
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/hosted-boot.d.ts +3 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +14 -0
- package/dist/hosted-project-folder-seed.d.ts +18 -0
- package/dist/hosted-project-folder-seed.d.ts.map +1 -0
- package/dist/hosted-project-folder-seed.js +78 -0
- package/dist/secret-redaction.d.ts +14 -0
- package/dist/secret-redaction.d.ts.map +1 -1
- package/dist/secret-redaction.js +25 -0
- package/dist/skilled-proposer-launch.d.ts +5 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +104 -46
- package/package.json +1 -1
- package/src/hosted-boot.ts +18 -0
- package/src/hosted-project-folder-seed.ts +95 -0
- package/src/secret-redaction.ts +30 -0
- package/src/skilled-proposer-launch.ts +78 -36
- package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +123 -0
package/dist/hosted-boot.d.ts
CHANGED
|
@@ -160,6 +160,9 @@ export interface ResolvedSession {
|
|
|
160
160
|
agentSessionId: string | null;
|
|
161
161
|
seedOrigin: 'team' | 'session';
|
|
162
162
|
projectId: string | null;
|
|
163
|
+
/** The session's own project's slug and name (ORI-2609), when the server sends them. */
|
|
164
|
+
projectSlug: string | null;
|
|
165
|
+
projectName: string | null;
|
|
163
166
|
/** Session lifetime in minutes (from client_info), if the coordinator recorded
|
|
164
167
|
* it — used to derive the per-prompt max-duration cap (ALI-1061). */
|
|
165
168
|
durationMinutes: number | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-boot.d.ts","sourceRoot":"","sources":["../src/hosted-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAmBH,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAEtB,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"hosted-boot.d.ts","sourceRoot":"","sources":["../src/hosted-boot.ts"],"names":[],"mappings":"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"}
|
package/dist/hosted-boot.js
CHANGED
|
@@ -39,6 +39,7 @@ import { INJECTED_ENV_VARS_ENV, closeHostedLoopSession, runHostedLoop, runHosted
|
|
|
39
39
|
import { DEFAULT_EGRESS_CANARY_HOST, DEFAULT_HOSTED_MODEL } from './hosted-loop-lifecycle.js';
|
|
40
40
|
import { composeHostedAnswerPrompt } from './hosted-question.js';
|
|
41
41
|
import { resumeRunEventSink } from './hosted-run-event-sink.js';
|
|
42
|
+
import { seedProjectFolder } from './hosted-project-folder-seed.js';
|
|
42
43
|
import { stageOrizuSkill } from './hosted-skill-staging.js';
|
|
43
44
|
import { openHostedLiveChannel } from './hosted-live-channel.js';
|
|
44
45
|
/** Non-secret placeholder model key (firewall brokers the real org key on
|
|
@@ -381,6 +382,8 @@ export async function resolveSession(opts) {
|
|
|
381
382
|
// A server with copying switched off omits seedOrigin; no source is bound.
|
|
382
383
|
seedOrigin: session.seedOrigin === 'session' ? 'session' : 'team',
|
|
383
384
|
projectId: asString(session.projectId),
|
|
385
|
+
projectSlug: asString(session.projectSlug),
|
|
386
|
+
projectName: asString(session.projectName),
|
|
384
387
|
};
|
|
385
388
|
}
|
|
386
389
|
export const HOSTED_SESSION_DEFAULT_POLL_MS = 5_000;
|
|
@@ -1052,6 +1055,17 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
|
1052
1055
|
exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email]);
|
|
1053
1056
|
if (!wokenDisk && !sessionCopyFilled)
|
|
1054
1057
|
log(`cloned ${session.repoBranch}`);
|
|
1058
|
+
// 5a — ORI-2609: the team copy holds only the projects the team seed knew
|
|
1059
|
+
// about. Write this session's project folder if it is missing, before the
|
|
1060
|
+
// agent starts. Never fails the boot. A woken disk is the agent's own saved
|
|
1061
|
+
// state, so a folder it removed there is not put back.
|
|
1062
|
+
log(wokenDisk
|
|
1063
|
+
? 'project folder left as the woken disk has it'
|
|
1064
|
+
: seedProjectFolder({
|
|
1065
|
+
workTree: workspaceDir,
|
|
1066
|
+
project: { id: session.projectId, slug: session.projectSlug, name: session.projectName },
|
|
1067
|
+
exec: (cmd, args) => exec(cmd, args),
|
|
1068
|
+
}));
|
|
1055
1069
|
// 5b — Stage the orizu skill into the cloned repo so the agent discovers the
|
|
1056
1070
|
// Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
|
|
1057
1071
|
// (one resolution chain + the harvest-safe .git/info/exclude append). Non-fatal:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ProjectFolderSeedProject {
|
|
2
|
+
id: string | null;
|
|
3
|
+
slug: string | null;
|
|
4
|
+
name: string | null;
|
|
5
|
+
}
|
|
6
|
+
type SeedExec = (cmd: string, args: string[]) => {
|
|
7
|
+
status: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
};
|
|
11
|
+
/** Returns the one line the boot logs. */
|
|
12
|
+
export declare function seedProjectFolder(input: {
|
|
13
|
+
workTree: string;
|
|
14
|
+
project: ProjectFolderSeedProject;
|
|
15
|
+
exec: SeedExec;
|
|
16
|
+
}): string;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=hosted-project-folder-seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-project-folder-seed.d.ts","sourceRoot":"","sources":["../src/hosted-project-folder-seed.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,KAAK,QAAQ,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAgBnG,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,wBAAwB,CAAA;IACjC,IAAI,EAAE,QAAQ,CAAA;CACf,GAAG,MAAM,CA8CT"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORI-2609: every project of an approved team shares the team's repository,
|
|
3
|
+
* but the team seed wrote only the first project's folder. A hosted session
|
|
4
|
+
* for another project writes that project's folder into its own checkout at
|
|
5
|
+
* boot, exactly as the team seed would have, and commits it locally. The
|
|
6
|
+
* session's normal save carries the commit; nothing is pushed here.
|
|
7
|
+
*
|
|
8
|
+
* A folder that is already there, whole or partial, is never touched, so a
|
|
9
|
+
* second session never duplicates or overwrites one. Never throws: a seed
|
|
10
|
+
* failure is reported for the boot to log, and the session still starts.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { buildSeedContractFiles, SEED_COMMIT_AUTHOR_EMAIL, SEED_COMMIT_AUTHOR_NAME, SEED_COMMIT_DATE, } from './workspace-bootstrap-seed.js';
|
|
15
|
+
// One plain folder name: no separators, no traversal, no leading dot or dash.
|
|
16
|
+
const SAFE_PROJECT_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
17
|
+
function readTeamFacts(workTree) {
|
|
18
|
+
// The team manifest the team seed wrote is the source of the team facts, so
|
|
19
|
+
// the new project's manifest agrees with the repository it lands in.
|
|
20
|
+
const manifest = JSON.parse(readFileSync(join(workTree, 'orizu.team.json'), 'utf8'));
|
|
21
|
+
if (typeof manifest.slug !== 'string' || manifest.slug.length === 0) {
|
|
22
|
+
throw new Error('orizu.team.json carries no team slug');
|
|
23
|
+
}
|
|
24
|
+
// The team id does not appear in project files; it is passed for completeness.
|
|
25
|
+
return { id: typeof manifest.teamId === 'string' ? manifest.teamId : '', slug: manifest.slug };
|
|
26
|
+
}
|
|
27
|
+
/** Returns the one line the boot logs. */
|
|
28
|
+
export function seedProjectFolder(input) {
|
|
29
|
+
const { workTree, project, exec } = input;
|
|
30
|
+
if (!project.id)
|
|
31
|
+
return 'project folder skipped: the session names no project';
|
|
32
|
+
// Without the slug there is no folder name; the server leaves it out when it
|
|
33
|
+
// could not read the project.
|
|
34
|
+
if (!project.slug)
|
|
35
|
+
return 'project folder skipped: the server sent no project slug';
|
|
36
|
+
try {
|
|
37
|
+
const slug = project.slug;
|
|
38
|
+
if (!SAFE_PROJECT_SLUG.test(slug))
|
|
39
|
+
throw new Error('the project slug is not a plain folder name');
|
|
40
|
+
const folder = `projects/${slug}`;
|
|
41
|
+
const projectsDir = join(workTree, 'projects');
|
|
42
|
+
if (existsSync(projectsDir) && !lstatSync(projectsDir).isDirectory()) {
|
|
43
|
+
throw new Error('projects/ is not a folder');
|
|
44
|
+
}
|
|
45
|
+
if (existsSync(join(workTree, folder)))
|
|
46
|
+
return `project folder ${folder}/ already present`;
|
|
47
|
+
// Byte-for-byte the files the team seed writes for this project.
|
|
48
|
+
const files = buildSeedContractFiles({
|
|
49
|
+
team: readTeamFacts(workTree),
|
|
50
|
+
projects: [{ id: project.id, slug, name: project.name }],
|
|
51
|
+
}).filter(file => file.path.startsWith(`${folder}/`));
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const target = join(workTree, file.path);
|
|
54
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
55
|
+
writeFileSync(target, file.bytes, { mode: 0o644 });
|
|
56
|
+
}
|
|
57
|
+
// No hook the checkout's config names runs inside the boot (as in harvest).
|
|
58
|
+
// The exec seam takes no `env` option, so `env` pins the commit dates.
|
|
59
|
+
const git = (step, ...args) => {
|
|
60
|
+
const result = exec('env', [
|
|
61
|
+
`GIT_AUTHOR_DATE=${SEED_COMMIT_DATE}`, `GIT_COMMITTER_DATE=${SEED_COMMIT_DATE}`,
|
|
62
|
+
'git', '-C', workTree, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args,
|
|
63
|
+
]);
|
|
64
|
+
if (result.status !== 0)
|
|
65
|
+
throw new Error(`git ${step} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
66
|
+
};
|
|
67
|
+
git('add', 'add', '--', folder);
|
|
68
|
+
// Paths after `--` commit only this folder, whatever else is staged. A fixed
|
|
69
|
+
// author, committer and date make every session that starts from the same
|
|
70
|
+
// team head produce the same commit, so two sessions' publishes share it
|
|
71
|
+
// rather than conflict on files neither of them touched.
|
|
72
|
+
git('commit', '-c', 'commit.gpgsign=false', '-c', `user.name=${SEED_COMMIT_AUTHOR_NAME}`, '-c', `user.email=${SEED_COMMIT_AUTHOR_EMAIL}`, 'commit', '--quiet', '--no-verify', '-m', `Add the ${slug} project folder`, '--', folder);
|
|
73
|
+
return `project folder ${folder}/ seeded`;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return `project folder seed failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -28,6 +28,20 @@
|
|
|
28
28
|
* event payload can be passed through before it is sent to the RunAPI.
|
|
29
29
|
*/
|
|
30
30
|
export declare const REDACTION_PLACEHOLDER = "[redacted]";
|
|
31
|
+
/**
|
|
32
|
+
* The shape pass alone, for servers that hold no secret values: the web app
|
|
33
|
+
* and the session coordinator scrub agent text with it before it reaches a
|
|
34
|
+
* browser (ORI-1905). One list of shapes for the CLI and both servers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactSecretShapes(input: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* For reply text shown as it is written: each snapshot is the reply so far, so
|
|
39
|
+
* one can end part-way through a token, before it is long enough for its shape
|
|
40
|
+
* to match. Such a tail is redacted too. The saved read shows a reply's last
|
|
41
|
+
* snapshot, so a finished reply that really ends in a bare `sk-` loses those
|
|
42
|
+
* characters there as well.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactStreamedTextShapes(input: string): string;
|
|
31
45
|
export interface RedactOptions {
|
|
32
46
|
/** Verbatim secret values to strip wherever they appear (bearer, tokens). */
|
|
33
47
|
secrets?: readonly string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-redaction.d.ts","sourceRoot":"","sources":["../src/secret-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,eAAO,MAAM,qBAAqB,eAAe,CAAA;AAgDjD,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,aAAkB,GAAG,CAAC,CAGzE;AAmBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQjF"}
|
|
1
|
+
{"version":3,"file":"secret-redaction.d.ts","sourceRoot":"","sources":["../src/secret-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,eAAO,MAAM,qBAAqB,eAAe,CAAA;AAgDjD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAUD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,aAAkB,GAAG,CAAC,CAGzE;AAmBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQjF"}
|
package/dist/secret-redaction.js
CHANGED
|
@@ -70,6 +70,31 @@ function redactString(input, secrets) {
|
|
|
70
70
|
}
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The shape pass alone, for servers that hold no secret values: the web app
|
|
75
|
+
* and the session coordinator scrub agent text with it before it reaches a
|
|
76
|
+
* browser (ORI-1905). One list of shapes for the CLI and both servers.
|
|
77
|
+
*/
|
|
78
|
+
export function redactSecretShapes(input) {
|
|
79
|
+
return redactString(input, []);
|
|
80
|
+
}
|
|
81
|
+
// A known credential prefix at the end of the text, followed by fewer
|
|
82
|
+
// characters than its shape above needs: the start of a token still being
|
|
83
|
+
// written. Each run is bounded, so the scan stays linear in the text, and a
|
|
84
|
+
// longer run that is not a token (a path, a word) is left alone. `Bearer` only
|
|
85
|
+
// counts in an Authorization header, so prose like "a Bearer token" keeps its
|
|
86
|
+
// last word.
|
|
87
|
+
const OPEN_TOKEN_AT_END = /(?:\bgithub_pat_[A-Za-z0-9_]{0,19}|\bgh[posur]_[A-Za-z0-9_]{0,19}|\borizu_(?:agent|pat)_[A-Za-z0-9_-]{0,9}|\bsk-[A-Za-z0-9_-]{0,19}|\b(authorization:\s*bearer)\s+[A-Za-z0-9._~+/-]{0,15})$/i;
|
|
88
|
+
/**
|
|
89
|
+
* For reply text shown as it is written: each snapshot is the reply so far, so
|
|
90
|
+
* one can end part-way through a token, before it is long enough for its shape
|
|
91
|
+
* to match. Such a tail is redacted too. The saved read shows a reply's last
|
|
92
|
+
* snapshot, so a finished reply that really ends in a bare `sk-` loses those
|
|
93
|
+
* characters there as well.
|
|
94
|
+
*/
|
|
95
|
+
export function redactStreamedTextShapes(input) {
|
|
96
|
+
return redactSecretShapes(input).replace(OPEN_TOKEN_AT_END, (_match, header) => header ? `${header} ${REDACTION_PLACEHOLDER}` : REDACTION_PLACEHOLDER);
|
|
97
|
+
}
|
|
73
98
|
/**
|
|
74
99
|
* Deep-redact a JSON-ish value. Strings are scrubbed; arrays/objects are walked;
|
|
75
100
|
* everything else is returned as-is. Object KEYS are never treated as secrets
|
|
@@ -23,6 +23,11 @@ export interface SkilledProposerChildOutput {
|
|
|
23
23
|
stdout: (chunk: string) => void;
|
|
24
24
|
stderr: (chunk: string) => void;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Run the optimizer, passing a stop signal on to it. On a stop signal the CLI
|
|
28
|
+
* waits until the optimizer exits (capped at CHILD_STOP_WAIT_MS, then kills
|
|
29
|
+
* it) and then ends by that same signal.
|
|
30
|
+
*/
|
|
26
31
|
export declare function spawnSkilledProposerChild(command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv, onSpawn?: () => Promise<void>, output?: SkilledProposerChildOutput): Promise<{
|
|
27
32
|
status: number | null;
|
|
28
33
|
error?: Error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAO3D,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClD,oBAAoB,EAAE,OAAO,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAClC;AACD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAC,UAAU,CAAA;IAC9B,MAAM,CAAC,EAAE,yBAAyB,CAAA;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;
|
|
1
|
+
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAO3D,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClD,oBAAoB,EAAE,OAAO,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAClC;AACD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAC,UAAU,CAAA;IAC9B,MAAM,CAAC,EAAE,yBAAyB,CAAA;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAsBD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU,EACnF,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,MAAM,CAAC,EAAE,0BAA0B;YA0CX,MAAM,GAAG,IAAI;YAAU,KAAK;GAkDrD;AAwCD,UAAU,aAAa;IAAG,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE;AAQ9F,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,UAAQ,GAAG,MAAM,CAepF;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,CAAC,UAAU,GAC7B,qBAAqB,CAyBvB"}
|
|
@@ -23,70 +23,128 @@ function materializeConfigPayload(engine, environment) {
|
|
|
23
23
|
}
|
|
24
24
|
return () => rmSync(root, { recursive: true, force: true });
|
|
25
25
|
}
|
|
26
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'];
|
|
27
|
+
// The optimizer marks its run stopped before exiting (at most 10 s for the
|
|
28
|
+
// status write); past this it is killed so nothing is left running (ORI-2402).
|
|
29
|
+
const CHILD_STOP_WAIT_MS = 15_000;
|
|
30
|
+
/**
|
|
31
|
+
* Run the optimizer, passing a stop signal on to it. On a stop signal the CLI
|
|
32
|
+
* waits until the optimizer exits (capped at CHILD_STOP_WAIT_MS, then kills
|
|
33
|
+
* it) and then ends by that same signal.
|
|
34
|
+
*/
|
|
26
35
|
export async function spawnSkilledProposerChild(command, args, engine, environment, onSpawn, output) {
|
|
27
36
|
const childEnvironment = { ...environment };
|
|
28
|
-
const shouldMaterialize = engine === 'official'
|
|
29
|
-
&& childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
30
|
-
&& childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined;
|
|
31
|
-
if (!shouldMaterialize)
|
|
32
|
-
return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment });
|
|
33
37
|
let child;
|
|
34
38
|
let cleanup = () => { };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
let stopSignal;
|
|
40
|
+
// On the terminal path the optimizer leads its own process group, so a stop
|
|
41
|
+
// also reaches its runners, which may run in worker threads it can't stop.
|
|
42
|
+
let childGroup;
|
|
43
|
+
const forward = (signal) => {
|
|
44
|
+
if (childGroup) {
|
|
39
45
|
try {
|
|
40
|
-
|
|
46
|
+
process.kill(-childGroup, signal);
|
|
47
|
+
return;
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
catch { /* group already gone */ }
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
child?.kill(signal);
|
|
53
|
+
}
|
|
54
|
+
catch { /* already exited */ }
|
|
55
|
+
};
|
|
56
|
+
let giveUp;
|
|
57
|
+
const removeHandlers = () => { for (const entry of handlers)
|
|
58
|
+
process.off(entry.signal, entry.handle); };
|
|
59
|
+
const endBySignal = (signal) => {
|
|
60
|
+
if (giveUp)
|
|
61
|
+
clearTimeout(giveUp);
|
|
62
|
+
removeHandlers();
|
|
63
|
+
cleanup();
|
|
64
|
+
// With our handlers gone, the signal's default action ends the process.
|
|
65
|
+
process.kill(process.pid, signal);
|
|
66
|
+
return new Promise(() => { });
|
|
67
|
+
};
|
|
68
|
+
const handlers = STOP_SIGNALS.map(signal => ({
|
|
69
|
+
signal,
|
|
70
|
+
handle: () => {
|
|
71
|
+
if (!child) {
|
|
72
|
+
void endBySignal(signal);
|
|
73
|
+
return;
|
|
51
74
|
}
|
|
75
|
+
forward(signal);
|
|
76
|
+
if (stopSignal)
|
|
77
|
+
return;
|
|
78
|
+
stopSignal = signal;
|
|
79
|
+
// The child read the private payload at start and a stopping run
|
|
80
|
+
// proposes nothing, so it goes now rather than when the child exits.
|
|
81
|
+
cleanup();
|
|
82
|
+
giveUp = setTimeout(() => forward('SIGKILL'), CHILD_STOP_WAIT_MS);
|
|
52
83
|
},
|
|
53
84
|
}));
|
|
54
85
|
for (const entry of handlers)
|
|
55
|
-
process.
|
|
86
|
+
process.on(entry.signal, entry.handle);
|
|
56
87
|
try {
|
|
57
88
|
cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup;
|
|
58
89
|
await new Promise(resolve => setImmediate(resolve));
|
|
90
|
+
let result;
|
|
59
91
|
if (output) {
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const spawnedChild = spawn(command, args, {
|
|
63
|
-
stdio: 'inherit',
|
|
64
|
-
env: childEnvironment,
|
|
65
|
-
});
|
|
66
|
-
child = spawnedChild;
|
|
67
|
-
const spawned = new Promise((resolve, reject) => {
|
|
68
|
-
spawnedChild.once('spawn', resolve);
|
|
69
|
-
spawnedChild.once('error', reject);
|
|
70
|
-
});
|
|
71
|
-
const closed = new Promise(resolve => {
|
|
72
|
-
let error;
|
|
73
|
-
spawnedChild.once('error', cause => { error = cause; });
|
|
74
|
-
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }));
|
|
75
|
-
});
|
|
76
|
-
await spawned;
|
|
77
|
-
try {
|
|
78
|
-
await onSpawn?.();
|
|
92
|
+
result = await runPipedChild(command, args, childEnvironment, async () => { await onSpawn?.(); }, output, spawnedChild => { child = spawnedChild; });
|
|
79
93
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
else {
|
|
95
|
+
// stdin is not passed on: a process outside the terminal's foreground
|
|
96
|
+
// group that read the terminal would be stopped by it.
|
|
97
|
+
// Outside the CLI's group, the optimizer would outlive a SIGKILL of the
|
|
98
|
+
// CLI; it watches for that and stops itself.
|
|
99
|
+
const spawnedChild = spawn(command, args, {
|
|
100
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
101
|
+
env: { ...childEnvironment, ORIZU_STOP_WITH_PARENT: '1' },
|
|
102
|
+
detached: true,
|
|
103
|
+
});
|
|
104
|
+
child = spawnedChild;
|
|
105
|
+
childGroup = spawnedChild.pid;
|
|
106
|
+
const spawned = new Promise((resolve, reject) => {
|
|
107
|
+
spawnedChild.once('spawn', resolve);
|
|
108
|
+
spawnedChild.once('error', reject);
|
|
109
|
+
});
|
|
110
|
+
const closed = new Promise(resolve => {
|
|
111
|
+
let error;
|
|
112
|
+
spawnedChild.once('error', cause => { error = cause; });
|
|
113
|
+
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }));
|
|
114
|
+
});
|
|
115
|
+
// A launch failure (e.g. ENOENT) is returned, not thrown, so callers
|
|
116
|
+
// name the engine as they did with spawnSync.
|
|
117
|
+
try {
|
|
118
|
+
await spawned;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { status: null, error: error };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await onSpawn?.();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
spawnedChild.kill('SIGTERM');
|
|
128
|
+
await closed;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
result = await closed;
|
|
132
|
+
// A runner that ignored the stop must not outlive the optimizer.
|
|
133
|
+
if (stopSignal) {
|
|
134
|
+
try {
|
|
135
|
+
process.kill(-spawnedChild.pid, 'SIGKILL');
|
|
136
|
+
}
|
|
137
|
+
catch { /* group already gone */ }
|
|
138
|
+
}
|
|
84
139
|
}
|
|
85
|
-
|
|
140
|
+
if (stopSignal)
|
|
141
|
+
return await endBySignal(stopSignal);
|
|
142
|
+
return result;
|
|
86
143
|
}
|
|
87
144
|
finally {
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
if (giveUp)
|
|
146
|
+
clearTimeout(giveUp);
|
|
147
|
+
removeHandlers();
|
|
90
148
|
cleanup();
|
|
91
149
|
}
|
|
92
150
|
}
|
package/package.json
CHANGED
package/src/hosted-boot.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
import { DEFAULT_EGRESS_CANARY_HOST, DEFAULT_HOSTED_MODEL } from './hosted-loop-lifecycle.js'
|
|
61
61
|
import { composeHostedAnswerPrompt } from './hosted-question.js'
|
|
62
62
|
import { resumeRunEventSink } from './hosted-run-event-sink.js'
|
|
63
|
+
import { seedProjectFolder } from './hosted-project-folder-seed.js'
|
|
63
64
|
import { stageOrizuSkill } from './hosted-skill-staging.js'
|
|
64
65
|
import { openHostedLiveChannel, type HostedLiveChannel } from './hosted-live-channel.js'
|
|
65
66
|
|
|
@@ -441,6 +442,9 @@ export interface ResolvedSession {
|
|
|
441
442
|
agentSessionId: string | null
|
|
442
443
|
seedOrigin: 'team' | 'session'
|
|
443
444
|
projectId: string | null
|
|
445
|
+
/** The session's own project's slug and name (ORI-2609), when the server sends them. */
|
|
446
|
+
projectSlug: string | null
|
|
447
|
+
projectName: string | null
|
|
444
448
|
/** Session lifetime in minutes (from client_info), if the coordinator recorded
|
|
445
449
|
* it — used to derive the per-prompt max-duration cap (ALI-1061). */
|
|
446
450
|
durationMinutes: number | null
|
|
@@ -547,6 +551,8 @@ export async function resolveSession(opts: {
|
|
|
547
551
|
// A server with copying switched off omits seedOrigin; no source is bound.
|
|
548
552
|
seedOrigin: session.seedOrigin === 'session' ? 'session' : 'team',
|
|
549
553
|
projectId: asString(session.projectId),
|
|
554
|
+
projectSlug: asString(session.projectSlug),
|
|
555
|
+
projectName: asString(session.projectName),
|
|
550
556
|
}
|
|
551
557
|
}
|
|
552
558
|
|
|
@@ -1342,6 +1348,18 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
|
1342
1348
|
exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email])
|
|
1343
1349
|
if (!wokenDisk && !sessionCopyFilled) log(`cloned ${session.repoBranch}`)
|
|
1344
1350
|
|
|
1351
|
+
// 5a — ORI-2609: the team copy holds only the projects the team seed knew
|
|
1352
|
+
// about. Write this session's project folder if it is missing, before the
|
|
1353
|
+
// agent starts. Never fails the boot. A woken disk is the agent's own saved
|
|
1354
|
+
// state, so a folder it removed there is not put back.
|
|
1355
|
+
log(wokenDisk
|
|
1356
|
+
? 'project folder left as the woken disk has it'
|
|
1357
|
+
: seedProjectFolder({
|
|
1358
|
+
workTree: workspaceDir,
|
|
1359
|
+
project: { id: session.projectId, slug: session.projectSlug, name: session.projectName },
|
|
1360
|
+
exec: (cmd, args) => exec(cmd, args),
|
|
1361
|
+
}))
|
|
1362
|
+
|
|
1345
1363
|
// 5b — Stage the orizu skill into the cloned repo so the agent discovers the
|
|
1346
1364
|
// Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
|
|
1347
1365
|
// (one resolution chain + the harvest-safe .git/info/exclude append). Non-fatal:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORI-2609: every project of an approved team shares the team's repository,
|
|
3
|
+
* but the team seed wrote only the first project's folder. A hosted session
|
|
4
|
+
* for another project writes that project's folder into its own checkout at
|
|
5
|
+
* boot, exactly as the team seed would have, and commits it locally. The
|
|
6
|
+
* session's normal save carries the commit; nothing is pushed here.
|
|
7
|
+
*
|
|
8
|
+
* A folder that is already there, whole or partial, is never touched, so a
|
|
9
|
+
* second session never duplicates or overwrites one. Never throws: a seed
|
|
10
|
+
* failure is reported for the boot to log, and the session still starts.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
buildSeedContractFiles,
|
|
17
|
+
SEED_COMMIT_AUTHOR_EMAIL,
|
|
18
|
+
SEED_COMMIT_AUTHOR_NAME,
|
|
19
|
+
SEED_COMMIT_DATE,
|
|
20
|
+
} from './workspace-bootstrap-seed.js'
|
|
21
|
+
|
|
22
|
+
export interface ProjectFolderSeedProject {
|
|
23
|
+
id: string | null
|
|
24
|
+
slug: string | null
|
|
25
|
+
name: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SeedExec = (cmd: string, args: string[]) => { status: number; stdout: string; stderr: string }
|
|
29
|
+
|
|
30
|
+
// One plain folder name: no separators, no traversal, no leading dot or dash.
|
|
31
|
+
const SAFE_PROJECT_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
32
|
+
|
|
33
|
+
function readTeamFacts(workTree: string): { id: string; slug: string } {
|
|
34
|
+
// The team manifest the team seed wrote is the source of the team facts, so
|
|
35
|
+
// the new project's manifest agrees with the repository it lands in.
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(workTree, 'orizu.team.json'), 'utf8')) as Record<string, unknown>
|
|
37
|
+
if (typeof manifest.slug !== 'string' || manifest.slug.length === 0) {
|
|
38
|
+
throw new Error('orizu.team.json carries no team slug')
|
|
39
|
+
}
|
|
40
|
+
// The team id does not appear in project files; it is passed for completeness.
|
|
41
|
+
return { id: typeof manifest.teamId === 'string' ? manifest.teamId : '', slug: manifest.slug }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns the one line the boot logs. */
|
|
45
|
+
export function seedProjectFolder(input: {
|
|
46
|
+
workTree: string
|
|
47
|
+
project: ProjectFolderSeedProject
|
|
48
|
+
exec: SeedExec
|
|
49
|
+
}): string {
|
|
50
|
+
const { workTree, project, exec } = input
|
|
51
|
+
if (!project.id) return 'project folder skipped: the session names no project'
|
|
52
|
+
// Without the slug there is no folder name; the server leaves it out when it
|
|
53
|
+
// could not read the project.
|
|
54
|
+
if (!project.slug) return 'project folder skipped: the server sent no project slug'
|
|
55
|
+
try {
|
|
56
|
+
const slug = project.slug
|
|
57
|
+
if (!SAFE_PROJECT_SLUG.test(slug)) throw new Error('the project slug is not a plain folder name')
|
|
58
|
+
const folder = `projects/${slug}`
|
|
59
|
+
const projectsDir = join(workTree, 'projects')
|
|
60
|
+
if (existsSync(projectsDir) && !lstatSync(projectsDir).isDirectory()) {
|
|
61
|
+
throw new Error('projects/ is not a folder')
|
|
62
|
+
}
|
|
63
|
+
if (existsSync(join(workTree, folder))) return `project folder ${folder}/ already present`
|
|
64
|
+
|
|
65
|
+
// Byte-for-byte the files the team seed writes for this project.
|
|
66
|
+
const files = buildSeedContractFiles({
|
|
67
|
+
team: readTeamFacts(workTree),
|
|
68
|
+
projects: [{ id: project.id, slug, name: project.name }],
|
|
69
|
+
}).filter(file => file.path.startsWith(`${folder}/`))
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const target = join(workTree, file.path)
|
|
72
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
73
|
+
writeFileSync(target, file.bytes, { mode: 0o644 })
|
|
74
|
+
}
|
|
75
|
+
// No hook the checkout's config names runs inside the boot (as in harvest).
|
|
76
|
+
// The exec seam takes no `env` option, so `env` pins the commit dates.
|
|
77
|
+
const git = (step: string, ...args: string[]) => {
|
|
78
|
+
const result = exec('env', [
|
|
79
|
+
`GIT_AUTHOR_DATE=${SEED_COMMIT_DATE}`, `GIT_COMMITTER_DATE=${SEED_COMMIT_DATE}`,
|
|
80
|
+
'git', '-C', workTree, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args,
|
|
81
|
+
])
|
|
82
|
+
if (result.status !== 0) throw new Error(`git ${step} failed: ${(result.stderr || result.stdout).trim()}`)
|
|
83
|
+
}
|
|
84
|
+
git('add', 'add', '--', folder)
|
|
85
|
+
// Paths after `--` commit only this folder, whatever else is staged. A fixed
|
|
86
|
+
// author, committer and date make every session that starts from the same
|
|
87
|
+
// team head produce the same commit, so two sessions' publishes share it
|
|
88
|
+
// rather than conflict on files neither of them touched.
|
|
89
|
+
git('commit', '-c', 'commit.gpgsign=false', '-c', `user.name=${SEED_COMMIT_AUTHOR_NAME}`, '-c', `user.email=${SEED_COMMIT_AUTHOR_EMAIL}`,
|
|
90
|
+
'commit', '--quiet', '--no-verify', '-m', `Add the ${slug} project folder`, '--', folder)
|
|
91
|
+
return `project folder ${folder}/ seeded`
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return `project folder seed failed: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/secret-redaction.ts
CHANGED
|
@@ -76,6 +76,36 @@ function redactString(input: string, secrets: readonly string[]): string {
|
|
|
76
76
|
return out
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The shape pass alone, for servers that hold no secret values: the web app
|
|
81
|
+
* and the session coordinator scrub agent text with it before it reaches a
|
|
82
|
+
* browser (ORI-1905). One list of shapes for the CLI and both servers.
|
|
83
|
+
*/
|
|
84
|
+
export function redactSecretShapes(input: string): string {
|
|
85
|
+
return redactString(input, [])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A known credential prefix at the end of the text, followed by fewer
|
|
89
|
+
// characters than its shape above needs: the start of a token still being
|
|
90
|
+
// written. Each run is bounded, so the scan stays linear in the text, and a
|
|
91
|
+
// longer run that is not a token (a path, a word) is left alone. `Bearer` only
|
|
92
|
+
// counts in an Authorization header, so prose like "a Bearer token" keeps its
|
|
93
|
+
// last word.
|
|
94
|
+
const OPEN_TOKEN_AT_END = /(?:\bgithub_pat_[A-Za-z0-9_]{0,19}|\bgh[posur]_[A-Za-z0-9_]{0,19}|\borizu_(?:agent|pat)_[A-Za-z0-9_-]{0,9}|\bsk-[A-Za-z0-9_-]{0,19}|\b(authorization:\s*bearer)\s+[A-Za-z0-9._~+/-]{0,15})$/i
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* For reply text shown as it is written: each snapshot is the reply so far, so
|
|
98
|
+
* one can end part-way through a token, before it is long enough for its shape
|
|
99
|
+
* to match. Such a tail is redacted too. The saved read shows a reply's last
|
|
100
|
+
* snapshot, so a finished reply that really ends in a bare `sk-` loses those
|
|
101
|
+
* characters there as well.
|
|
102
|
+
*/
|
|
103
|
+
export function redactStreamedTextShapes(input: string): string {
|
|
104
|
+
return redactSecretShapes(input).replace(OPEN_TOKEN_AT_END, (_match, header?: string) =>
|
|
105
|
+
header ? `${header} ${REDACTION_PLACEHOLDER}` : REDACTION_PLACEHOLDER
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
export interface RedactOptions {
|
|
80
110
|
/** Verbatim secret values to strip wherever they appear (bearer, tokens). */
|
|
81
111
|
secrets?: readonly string[]
|
|
@@ -49,39 +49,64 @@ function materializeConfigPayload(engine: GepaEngine, environment: NodeJS.Proces
|
|
|
49
49
|
return () => rmSync(root, { recursive: true, force: true })
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const
|
|
53
|
+
// The optimizer marks its run stopped before exiting (at most 10 s for the
|
|
54
|
+
// status write); past this it is killed so nothing is left running (ORI-2402).
|
|
55
|
+
const CHILD_STOP_WAIT_MS = 15_000
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run the optimizer, passing a stop signal on to it. On a stop signal the CLI
|
|
59
|
+
* waits until the optimizer exits (capped at CHILD_STOP_WAIT_MS, then kills
|
|
60
|
+
* it) and then ends by that same signal.
|
|
61
|
+
*/
|
|
52
62
|
export async function spawnSkilledProposerChild(
|
|
53
63
|
command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv,
|
|
54
64
|
onSpawn?: () => Promise<void>,
|
|
55
65
|
output?: SkilledProposerChildOutput,
|
|
56
66
|
) {
|
|
57
67
|
const childEnvironment = { ...environment }
|
|
58
|
-
const shouldMaterialize = engine === 'official'
|
|
59
|
-
&& childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
60
|
-
&& childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined
|
|
61
|
-
if (!shouldMaterialize) return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment })
|
|
62
|
-
|
|
63
68
|
let child: ReturnType<typeof spawn> | PipedChild | undefined
|
|
64
69
|
let cleanup = () => {}
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
let stopSignal: NodeJS.Signals | undefined
|
|
71
|
+
// On the terminal path the optimizer leads its own process group, so a stop
|
|
72
|
+
// also reaches its runners, which may run in worker threads it can't stop.
|
|
73
|
+
let childGroup: number | undefined
|
|
74
|
+
const forward = (signal: NodeJS.Signals) => {
|
|
75
|
+
if (childGroup) {
|
|
76
|
+
try { process.kill(-childGroup, signal); return } catch { /* group already gone */ }
|
|
77
|
+
}
|
|
78
|
+
try { child?.kill(signal) } catch { /* already exited */ }
|
|
79
|
+
}
|
|
80
|
+
let giveUp: ReturnType<typeof setTimeout> | undefined
|
|
81
|
+
const removeHandlers = () => { for (const entry of handlers) process.off(entry.signal, entry.handle) }
|
|
82
|
+
const endBySignal = (signal: NodeJS.Signals): Promise<never> => {
|
|
83
|
+
if (giveUp) clearTimeout(giveUp)
|
|
84
|
+
removeHandlers()
|
|
85
|
+
cleanup()
|
|
86
|
+
// With our handlers gone, the signal's default action ends the process.
|
|
87
|
+
process.kill(process.pid, signal)
|
|
88
|
+
return new Promise<never>(() => {})
|
|
89
|
+
}
|
|
90
|
+
const handlers = STOP_SIGNALS.map(signal => ({
|
|
67
91
|
signal,
|
|
68
92
|
handle: () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
if (!child) { void endBySignal(signal); return }
|
|
94
|
+
forward(signal)
|
|
95
|
+
if (stopSignal) return
|
|
96
|
+
stopSignal = signal
|
|
97
|
+
// The child read the private payload at start and a stopping run
|
|
98
|
+
// proposes nothing, so it goes now rather than when the child exits.
|
|
99
|
+
cleanup()
|
|
100
|
+
giveUp = setTimeout(() => forward('SIGKILL'), CHILD_STOP_WAIT_MS)
|
|
77
101
|
},
|
|
78
102
|
}))
|
|
79
|
-
for (const entry of handlers) process.
|
|
103
|
+
for (const entry of handlers) process.on(entry.signal, entry.handle)
|
|
80
104
|
try {
|
|
81
105
|
cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup
|
|
82
106
|
await new Promise<void>(resolve => setImmediate(resolve))
|
|
107
|
+
let result: { status: number | null, error?: Error }
|
|
83
108
|
if (output) {
|
|
84
|
-
|
|
109
|
+
result = await runPipedChild(
|
|
85
110
|
command,
|
|
86
111
|
args,
|
|
87
112
|
childEnvironment,
|
|
@@ -89,27 +114,44 @@ export async function spawnSkilledProposerChild(
|
|
|
89
114
|
output,
|
|
90
115
|
spawnedChild => { child = spawnedChild }
|
|
91
116
|
)
|
|
117
|
+
} else {
|
|
118
|
+
// stdin is not passed on: a process outside the terminal's foreground
|
|
119
|
+
// group that read the terminal would be stopped by it.
|
|
120
|
+
// Outside the CLI's group, the optimizer would outlive a SIGKILL of the
|
|
121
|
+
// CLI; it watches for that and stops itself.
|
|
122
|
+
const spawnedChild = spawn(command, args, {
|
|
123
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
124
|
+
env: { ...childEnvironment, ORIZU_STOP_WITH_PARENT: '1' },
|
|
125
|
+
detached: true,
|
|
126
|
+
})
|
|
127
|
+
child = spawnedChild
|
|
128
|
+
childGroup = spawnedChild.pid
|
|
129
|
+
const spawned = new Promise<void>((resolve, reject) => {
|
|
130
|
+
spawnedChild.once('spawn', resolve)
|
|
131
|
+
spawnedChild.once('error', reject)
|
|
132
|
+
})
|
|
133
|
+
const closed = new Promise<{ status: number | null, error?: Error }>(resolve => {
|
|
134
|
+
let error: Error | undefined
|
|
135
|
+
spawnedChild.once('error', cause => { error = cause })
|
|
136
|
+
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }))
|
|
137
|
+
})
|
|
138
|
+
// A launch failure (e.g. ENOENT) is returned, not thrown, so callers
|
|
139
|
+
// name the engine as they did with spawnSync.
|
|
140
|
+
try { await spawned }
|
|
141
|
+
catch (error) { return { status: null, error: error as Error } }
|
|
142
|
+
try { await onSpawn?.() }
|
|
143
|
+
catch (error) { spawnedChild.kill('SIGTERM'); await closed; throw error }
|
|
144
|
+
result = await closed
|
|
145
|
+
// A runner that ignored the stop must not outlive the optimizer.
|
|
146
|
+
if (stopSignal) {
|
|
147
|
+
try { process.kill(-spawnedChild.pid!, 'SIGKILL') } catch { /* group already gone */ }
|
|
148
|
+
}
|
|
92
149
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
env: childEnvironment,
|
|
96
|
-
})
|
|
97
|
-
child = spawnedChild
|
|
98
|
-
const spawned = new Promise<void>((resolve, reject) => {
|
|
99
|
-
spawnedChild.once('spawn', resolve)
|
|
100
|
-
spawnedChild.once('error', reject)
|
|
101
|
-
})
|
|
102
|
-
const closed = new Promise<{ status: number | null, error?: Error }>(resolve => {
|
|
103
|
-
let error: Error | undefined
|
|
104
|
-
spawnedChild.once('error', cause => { error = cause })
|
|
105
|
-
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }))
|
|
106
|
-
})
|
|
107
|
-
await spawned
|
|
108
|
-
try { await onSpawn?.() }
|
|
109
|
-
catch (error) { spawnedChild.kill('SIGTERM'); await closed; throw error }
|
|
110
|
-
return await closed
|
|
150
|
+
if (stopSignal) return await endBySignal(stopSignal)
|
|
151
|
+
return result
|
|
111
152
|
} finally {
|
|
112
|
-
|
|
153
|
+
if (giveUp) clearTimeout(giveUp)
|
|
154
|
+
removeHandlers()
|
|
113
155
|
cleanup()
|
|
114
156
|
}
|
|
115
157
|
}
|
|
@@ -4,6 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import contextmanager
|
|
7
12
|
from pathlib import Path
|
|
8
13
|
from typing import Any
|
|
9
14
|
from uuid import uuid4
|
|
@@ -431,7 +436,120 @@ def _result_summary(*, run_id: str, best_id: str, best_score: float, seed_score:
|
|
|
431
436
|
}
|
|
432
437
|
|
|
433
438
|
|
|
439
|
+
_STOP_SIGNALS = (signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT)
|
|
440
|
+
# One Ctrl-C reaches the optimizer twice: from the terminal and again from the
|
|
441
|
+
# CLI that passes it on. A repeat this soon after the first is the same press.
|
|
442
|
+
_REPEAT_WINDOW_SECONDS = 1.0
|
|
443
|
+
_STATUS_WRITE_SECONDS = 10.0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class RunInterrupted(BaseException):
|
|
447
|
+
"""A stop signal (Ctrl-C, SIGTERM, a closed terminal) reached the run."""
|
|
448
|
+
|
|
449
|
+
def __init__(self, signal_name: str, signum: int) -> None:
|
|
450
|
+
super().__init__(signal_name)
|
|
451
|
+
self.signal_name = signal_name
|
|
452
|
+
self.signum = signum
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _end_by_signal(signum: int) -> None:
|
|
456
|
+
"""Exit the way an unhandled signal would, so callers see 128 + its number."""
|
|
457
|
+
sys.stdout.flush()
|
|
458
|
+
sys.stderr.flush()
|
|
459
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
460
|
+
os.kill(os.getpid(), signum)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@contextmanager
|
|
464
|
+
def interrupt_signals():
|
|
465
|
+
"""Turn the first stop signal into ``RunInterrupted``; a later one ends at once."""
|
|
466
|
+
first_at: list[float] = []
|
|
467
|
+
|
|
468
|
+
def handle(signum: int, _frame: Any) -> None:
|
|
469
|
+
if not first_at:
|
|
470
|
+
first_at.append(time.monotonic())
|
|
471
|
+
if os.getpgrp() == os.getpid():
|
|
472
|
+
# Runners may run in worker threads, which this interrupt does
|
|
473
|
+
# not reach; when we lead the process group, stop them too.
|
|
474
|
+
# Our own copy of the signal falls in the repeat window.
|
|
475
|
+
try:
|
|
476
|
+
os.killpg(os.getpgrp(), signum)
|
|
477
|
+
except OSError:
|
|
478
|
+
pass
|
|
479
|
+
raise RunInterrupted(signal.Signals(signum).name, signum)
|
|
480
|
+
if time.monotonic() - first_at[0] < _REPEAT_WINDOW_SECONDS:
|
|
481
|
+
return
|
|
482
|
+
_end_by_signal(signum)
|
|
483
|
+
|
|
484
|
+
previous = {signum: signal.getsignal(signum) for signum in _STOP_SIGNALS}
|
|
485
|
+
for signum in _STOP_SIGNALS:
|
|
486
|
+
signal.signal(signum, handle)
|
|
487
|
+
try:
|
|
488
|
+
yield
|
|
489
|
+
finally:
|
|
490
|
+
for signum, handler in previous.items():
|
|
491
|
+
signal.signal(signum, handler)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _stop_when_parent_exits() -> None:
|
|
495
|
+
"""Stop like SIGTERM once the CLI that started us is gone (e.g. SIGKILLed).
|
|
496
|
+
|
|
497
|
+
The CLI puts the optimizer in its own process group, so a signal sent to
|
|
498
|
+
the CLI's group no longer reaches it; without this it would keep running.
|
|
499
|
+
"""
|
|
500
|
+
parent = os.getppid()
|
|
501
|
+
|
|
502
|
+
def watch() -> None:
|
|
503
|
+
while os.getppid() == parent:
|
|
504
|
+
time.sleep(0.5)
|
|
505
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
|
506
|
+
|
|
507
|
+
threading.Thread(target=watch, daemon=True).start()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _mark_interrupted_run(client: OrizuClient, run_id: str, signal_name: str) -> None:
|
|
511
|
+
"""Mark a stopped local run cancelled once, waiting at most 10 s; never retry."""
|
|
512
|
+
outcome: list[BaseException | None] = []
|
|
513
|
+
|
|
514
|
+
def write() -> None:
|
|
515
|
+
try:
|
|
516
|
+
client.update_run(run_id, status="cancelled",
|
|
517
|
+
metadata={"stop_reason": "interrupted", "signal": signal_name})
|
|
518
|
+
outcome.append(None)
|
|
519
|
+
except BaseException as error: # reported below; the run still stops
|
|
520
|
+
outcome.append(error)
|
|
521
|
+
|
|
522
|
+
writer = threading.Thread(target=write, daemon=True)
|
|
523
|
+
writer.start()
|
|
524
|
+
writer.join(_STATUS_WRITE_SECONDS)
|
|
525
|
+
if not outcome or outcome[0] is not None:
|
|
526
|
+
print(
|
|
527
|
+
f"Run {run_id} may still show as running: run "
|
|
528
|
+
f"`orizu optimizations cancel {run_id}` to mark it stopped.",
|
|
529
|
+
file=sys.stderr, flush=True,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
434
533
|
def run_from_environment() -> dict[str, Any]:
|
|
534
|
+
"""Run official GEPA; a stopped local run is marked cancelled (ORI-2402).
|
|
535
|
+
|
|
536
|
+
A hosted run's final status belongs to its coordinator, which records it
|
|
537
|
+
before stopping the sandbox, so a hosted run writes no status here.
|
|
538
|
+
"""
|
|
539
|
+
started: dict[str, Any] = {}
|
|
540
|
+
with interrupt_signals():
|
|
541
|
+
if os.environ.get("ORIZU_STOP_WITH_PARENT") == "1":
|
|
542
|
+
_stop_when_parent_exits()
|
|
543
|
+
try:
|
|
544
|
+
return _run_from_environment(started)
|
|
545
|
+
except RunInterrupted as interrupt:
|
|
546
|
+
if started.get("run_id") and not started.get("hosted"):
|
|
547
|
+
_mark_interrupted_run(started["client"], started["run_id"], interrupt.signal_name)
|
|
548
|
+
_end_by_signal(interrupt.signum)
|
|
549
|
+
raise
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _run_from_environment(started: dict[str, Any]) -> dict[str, Any]:
|
|
435
553
|
"""Run official GEPA over the verified legacy runner subprocess contract.
|
|
436
554
|
|
|
437
555
|
ALI-1502 supplies these variables after verifying runner bytes. Scorer
|
|
@@ -542,6 +660,7 @@ def run_from_environment() -> dict[str, Any]:
|
|
|
542
660
|
model_config_settings_version_id=instruction_set.get("model_config_settings_version_id") if isinstance(instruction_set, dict) else None,
|
|
543
661
|
runner_version_id=prompt_context.runner_version_id,
|
|
544
662
|
)
|
|
663
|
+
started.update(client=client, run_id=run_id, hosted=hosted_run_id is not None)
|
|
545
664
|
print(json.dumps({"optimization_run_id": run_id}), flush=True)
|
|
546
665
|
# The frozen sink treats ``None`` as no local artifact writer. Keep that
|
|
547
666
|
# legacy behavior rather than maintaining a connector-local no-op logger.
|
|
@@ -728,6 +847,9 @@ def run_from_environment() -> dict[str, Any]:
|
|
|
728
847
|
total_tokens_out=int(getattr(reflection_lm, "total_tokens_out", 0)),
|
|
729
848
|
total_tokens=int(getattr(reflection_lm, "total_tokens", 0)),
|
|
730
849
|
)
|
|
850
|
+
# From here the run has its own final status; a stop must not
|
|
851
|
+
# overwrite it with "cancelled".
|
|
852
|
+
started.pop("run_id", None)
|
|
731
853
|
if budget_exhausted:
|
|
732
854
|
if hosted_run_id is None:
|
|
733
855
|
client.update_run(
|
|
@@ -764,6 +886,7 @@ def run_from_environment() -> dict[str, Any]:
|
|
|
764
886
|
temporary_result_path.replace(result_path)
|
|
765
887
|
return summary
|
|
766
888
|
except Exception as error:
|
|
889
|
+
started.pop("run_id", None)
|
|
767
890
|
if sink.failed:
|
|
768
891
|
sink.retry_failed_status()
|
|
769
892
|
elif hosted_run_id is None:
|