@rynx-ai/runtime 0.1.0 → 0.1.10-beta.2
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/claude/executor.d.ts +3 -5
- package/dist/claude/executor.js +3 -5
- package/dist/claude/native-bridge.d.ts +74 -17
- package/dist/claude/native-bridge.js +225 -30
- package/dist/claude/native-hook-main.js +327 -38
- package/dist/claude/native-hooks.d.ts +3 -2
- package/dist/claude/native-hooks.js +15 -6
- package/dist/claude/native-integration.d.ts +123 -16
- package/dist/claude/native-integration.js +624 -81
- package/dist/claude/settings.d.ts +8 -0
- package/dist/claude/settings.js +50 -0
- package/dist/claude/transcript.d.ts +2 -2
- package/dist/claude/transcript.js +14 -3
- package/dist/codex/rollout-synth.d.ts +8 -3
- package/dist/codex/rollout-synth.js +65 -32
- package/dist/codex-app-server/client.d.ts +27 -40
- package/dist/codex-app-server/client.js +1134 -99
- package/dist/codex-app-server/forwarder.d.ts +36 -10
- package/dist/codex-app-server/forwarder.js +146 -28
- package/dist/codex-app-server/mapping.d.ts +1 -1
- package/dist/codex-app-server/mapping.js +64 -5
- package/dist/codex-app-server/protocol.d.ts +269 -4
- package/dist/codex-app-server/transport.d.ts +20 -5
- package/dist/codex-app-server/transport.js +93 -40
- package/dist/codex-app-server/ws-channel.d.ts +3 -3
- package/dist/codex-app-server/ws-channel.js +23 -7
- package/dist/codex-child-env.js +33 -0
- package/dist/codex-home.d.ts +16 -6
- package/dist/codex-home.js +46 -15
- package/dist/codex-session-store.d.ts +2 -1
- package/dist/host.d.ts +38 -38
- package/dist/host.js +626 -121
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +13 -0
- package/dist/input-resources.js +67 -0
- package/dist/interactions.d.ts +61 -0
- package/dist/interactions.js +236 -0
- package/dist/models-catalog.d.ts +5 -13
- package/dist/models-catalog.js +60 -9
- package/dist/runner/child.d.ts +9 -1
- package/dist/runner/child.js +100 -19
- package/dist/runner/manager.d.ts +79 -11
- package/dist/runner/manager.js +423 -43
- package/dist/runner/protocol.d.ts +30 -11
- package/dist/runner-main.js +9 -6
- package/dist/runtime-status.js +1 -1
- package/dist/terminal/claude-tui.d.ts +8 -3
- package/dist/terminal/claude-tui.js +6 -2
- package/dist/terminal/codex-tui.d.ts +3 -3
- package/dist/terminal/codex-tui.js +1 -1
- package/dist/terminal/registry.d.ts +1 -1
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +6 -6
- package/dist/terminal/tmux.js +10 -10
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export { LocalAgentHost, CodexRuntimeError, SpawnCodexCommandRunner, FileCodexSe
|
|
|
8
8
|
export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from "./host.js";
|
|
9
9
|
export type { CodexSessionStore, CodexSessionRecord } from "./codex-session-store.js";
|
|
10
10
|
export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
11
|
-
export { RunnerManager } from "./runner/manager.js";
|
|
12
|
-
export type { RunnerManagerOptions, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
13
|
-
export type { InjectOutcome, TerminalRole } from "./runner/protocol.js";
|
|
11
|
+
export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
|
|
12
|
+
export type { RunnerManagerOptions, RunnerSessionContext, RunnerSessionContextProvider, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
13
|
+
export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
|
|
14
|
+
export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
|
|
14
15
|
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
15
16
|
export { listRuntimeModels } from "./models-catalog.js";
|
|
16
17
|
export { TmuxTerminal, isTmuxAvailable } from "./terminal/tmux.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { ensureCodexResumeRollout } from "./codex/rollout-synth.js";
|
|
|
9
9
|
// Runner subprocess layer: the parent-side manager (an `AgentExecutor` +
|
|
10
10
|
// `AgentCapabilities` that spawns per-session runner children) plus the wire
|
|
11
11
|
// types. The composition root uses `RunnerManager` in place of `LocalAgentHost`.
|
|
12
|
-
export { RunnerManager } from "./runner/manager.js";
|
|
12
|
+
export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
|
|
13
13
|
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
14
14
|
export { listRuntimeModels } from "./models-catalog.js";
|
|
15
15
|
// Live-terminal subsystem (Phase C): tmux-backed terminals + per-runner registry.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InputImageContentPart, RuntimeUserInput, UserContentPart } from "@rynx-ai/core";
|
|
2
|
+
import type { UserInput } from "./codex-app-server/protocol.js";
|
|
3
|
+
/** Convert the provider-native user echo back to resource references. Unknown
|
|
4
|
+
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
5
|
+
export declare function codexUserContent(input: readonly UserInput[]): UserContentPart[];
|
|
6
|
+
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
7
|
+
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
8
|
+
export declare function claudeInputText(input: RuntimeUserInput, attachmentToken?: string): string;
|
|
9
|
+
/** Extract only the one-time token. The host resolves it through its in-memory
|
|
10
|
+
* pending-injection map; user-authored lookalikes have no entry and remain text. */
|
|
11
|
+
export declare function claudeAttachmentToken(text: string): string | undefined;
|
|
12
|
+
export declare function runtimeUserContent(input: RuntimeUserInput): UserContentPart[];
|
|
13
|
+
export declare function managedImagePart(path: string): InputImageContentPart | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
const CLAUDE_ATTACHMENT_TOKEN = "RYNX_ATTACHMENT_SET";
|
|
4
|
+
const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering\.\]$/m;
|
|
5
|
+
/** Convert the provider-native user echo back to resource references. Unknown
|
|
6
|
+
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
7
|
+
export function codexUserContent(input) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const part of input) {
|
|
10
|
+
if (part.type === "text") {
|
|
11
|
+
if (part.text)
|
|
12
|
+
parts.push({ type: "input_text", text: part.text });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (part.type === "localImage") {
|
|
16
|
+
const image = managedImagePart(part.path);
|
|
17
|
+
if (image)
|
|
18
|
+
parts.push(image);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
24
|
+
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
25
|
+
export function claudeInputText(input, attachmentToken = `att_${randomUUID()}`) {
|
|
26
|
+
const images = input.content.filter((part) => part.type === "local_image");
|
|
27
|
+
const text = input.content
|
|
28
|
+
.filter((part) => part.type === "text")
|
|
29
|
+
.map((part) => part.text)
|
|
30
|
+
.join("");
|
|
31
|
+
if (images.length === 0)
|
|
32
|
+
return text;
|
|
33
|
+
if (!/^att_[0-9a-f-]{36}$/.test(attachmentToken)) {
|
|
34
|
+
throw new Error("Claude attachment token is invalid");
|
|
35
|
+
}
|
|
36
|
+
const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering.]`;
|
|
37
|
+
const markers = images.map((part) => `[[RYNX_IMAGE_RESOURCE ${JSON.stringify({ path: part.path })}]]`);
|
|
38
|
+
return [preamble, ...markers, text]
|
|
39
|
+
.filter((part, index) => index <= markers.length || part.length > 0)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** Extract only the one-time token. The host resolves it through its in-memory
|
|
43
|
+
* pending-injection map; user-authored lookalikes have no entry and remain text. */
|
|
44
|
+
export function claudeAttachmentToken(text) {
|
|
45
|
+
return text.match(CLAUDE_ATTACHMENT_TOKEN_RE)?.[1];
|
|
46
|
+
}
|
|
47
|
+
export function runtimeUserContent(input) {
|
|
48
|
+
return input.content.map((part) => part.type === "text"
|
|
49
|
+
? { type: "input_text", text: part.text }
|
|
50
|
+
: { ...part.resource });
|
|
51
|
+
}
|
|
52
|
+
export function managedImagePart(path) {
|
|
53
|
+
const extension = extname(path).toLowerCase();
|
|
54
|
+
const mediaType = extension === ".png"
|
|
55
|
+
? "image/png"
|
|
56
|
+
: extension === ".jpg" || extension === ".jpeg"
|
|
57
|
+
? "image/jpeg"
|
|
58
|
+
: extension === ".webp"
|
|
59
|
+
? "image/webp"
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!mediaType)
|
|
62
|
+
return undefined;
|
|
63
|
+
const resourceId = basename(path, extension);
|
|
64
|
+
if (!/^res_[0-9a-f-]{36}$/i.test(resourceId))
|
|
65
|
+
return undefined;
|
|
66
|
+
return { type: "input_image", resourceId, mediaType };
|
|
67
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type SessionInteractionRequest, type SessionInteractionResolution } from "@rynx-ai/core";
|
|
2
|
+
/** Provider-neutral lifecycle emitted by a native runtime adapter. */
|
|
3
|
+
export type RuntimeInteractionEvent = {
|
|
4
|
+
type: "requested";
|
|
5
|
+
request: SessionInteractionRequest;
|
|
6
|
+
/** Native Turn correlation, when the provider exposes it. */
|
|
7
|
+
turnId?: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "resolved";
|
|
10
|
+
interactionId: string;
|
|
11
|
+
resolution: SessionInteractionResolution;
|
|
12
|
+
turnId?: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "cancelled";
|
|
15
|
+
interactionId: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
turnId?: string;
|
|
18
|
+
};
|
|
19
|
+
export type RuntimeInteractionListener = (event: RuntimeInteractionEvent) => void;
|
|
20
|
+
/** Result of attempting to answer a pending native interaction. */
|
|
21
|
+
export type ResolveInteractionResult = {
|
|
22
|
+
disposition: "applied";
|
|
23
|
+
} | {
|
|
24
|
+
disposition: "already_resolved";
|
|
25
|
+
} | {
|
|
26
|
+
disposition: "not_found";
|
|
27
|
+
} | {
|
|
28
|
+
disposition: "invalid";
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
export type InteractionResolution = SessionInteractionResolution;
|
|
32
|
+
export declare const INTERACTION_LIMITS: {
|
|
33
|
+
readonly idBytes: 256;
|
|
34
|
+
readonly titleBytes: 1024;
|
|
35
|
+
readonly labelBytes: 1024;
|
|
36
|
+
readonly descriptionBytes: number;
|
|
37
|
+
readonly contextBytes: number;
|
|
38
|
+
readonly requestBytes: number;
|
|
39
|
+
readonly fields: 64;
|
|
40
|
+
readonly optionsPerField: 128;
|
|
41
|
+
readonly actions: 16;
|
|
42
|
+
readonly routingSkeletonBytes: number;
|
|
43
|
+
};
|
|
44
|
+
export type BoundInteractionRequestResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
request: SessionInteractionRequest;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/** Bound an untrusted provider request before it enters the canonical event
|
|
52
|
+
* stream. Lists that affect native answer routing are rejected when oversized;
|
|
53
|
+
* option lists may be truncated to a still-answerable subset. */
|
|
54
|
+
export declare function boundInteractionRequest(request: SessionInteractionRequest): BoundInteractionRequestResult;
|
|
55
|
+
/** Validate a provider-neutral resolution against the exact advertised form.
|
|
56
|
+
* Unknown keys and wrong answer shapes are rejected even for deny/cancel; the
|
|
57
|
+
* selected action's `requiresAnswers` controls only whether required fields may
|
|
58
|
+
* be omitted. */
|
|
59
|
+
export declare function validateInteractionResolution(request: SessionInteractionRequest, resolution: SessionInteractionResolution): string | undefined;
|
|
60
|
+
/** Secret values reach the native runtime but never enter canonical history. */
|
|
61
|
+
export declare function redactInteractionResolution(request: SessionInteractionRequest, resolution: SessionInteractionResolution): SessionInteractionResolution;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { boundSessionInteractionRouting, sessionInteractionRoutingSkeletonBytes, SESSION_INTERACTION_STRUCTURAL_LIMITS, } from "@rynx-ai/core";
|
|
2
|
+
export const INTERACTION_LIMITS = {
|
|
3
|
+
...SESSION_INTERACTION_STRUCTURAL_LIMITS,
|
|
4
|
+
idBytes: 256,
|
|
5
|
+
titleBytes: 1024,
|
|
6
|
+
labelBytes: 1024,
|
|
7
|
+
descriptionBytes: 8 * 1024,
|
|
8
|
+
contextBytes: 64 * 1024,
|
|
9
|
+
// SessionNormalizer carries the request twice (event + durable item). 240 KiB
|
|
10
|
+
// leaves 32 KiB for both wrappers under the daemon's 512 KiB event limit.
|
|
11
|
+
requestBytes: 240 * 1024,
|
|
12
|
+
};
|
|
13
|
+
/** UTF-8 byte-safe truncation: iteration is by Unicode code point, so neither a
|
|
14
|
+
* surrogate pair nor a multi-byte sequence is split. */
|
|
15
|
+
function truncateUtf8(value, maxBytes) {
|
|
16
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
17
|
+
return value;
|
|
18
|
+
const suffix = "…";
|
|
19
|
+
const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
|
|
20
|
+
let result = "";
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for (const char of value) {
|
|
23
|
+
const width = Buffer.byteLength(char, "utf8");
|
|
24
|
+
if (bytes + width > budget)
|
|
25
|
+
break;
|
|
26
|
+
result += char;
|
|
27
|
+
bytes += width;
|
|
28
|
+
}
|
|
29
|
+
return `${result}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
function validId(id) {
|
|
32
|
+
return id.length > 0 && Buffer.byteLength(id, "utf8") <= INTERACTION_LIMITS.idBytes;
|
|
33
|
+
}
|
|
34
|
+
/** Bound an untrusted provider request before it enters the canonical event
|
|
35
|
+
* stream. Lists that affect native answer routing are rejected when oversized;
|
|
36
|
+
* option lists may be truncated to a still-answerable subset. */
|
|
37
|
+
export function boundInteractionRequest(request) {
|
|
38
|
+
if (!validId(request.interactionId)) {
|
|
39
|
+
return { ok: false, reason: "interaction id exceeds the canonical limit" };
|
|
40
|
+
}
|
|
41
|
+
if (request.fields.length > INTERACTION_LIMITS.fields) {
|
|
42
|
+
return { ok: false, reason: "interaction has too many fields" };
|
|
43
|
+
}
|
|
44
|
+
if (request.actions.length === 0 || request.actions.length > INTERACTION_LIMITS.actions) {
|
|
45
|
+
return { ok: false, reason: "interaction has an invalid number of actions" };
|
|
46
|
+
}
|
|
47
|
+
const seenFields = new Set();
|
|
48
|
+
const fields = [];
|
|
49
|
+
for (const field of request.fields) {
|
|
50
|
+
if (!validId(field.id) || seenFields.has(field.id)) {
|
|
51
|
+
return { ok: false, reason: "interaction has an invalid or duplicate field id" };
|
|
52
|
+
}
|
|
53
|
+
seenFields.add(field.id);
|
|
54
|
+
const label = truncateUtf8(field.label, INTERACTION_LIMITS.labelBytes);
|
|
55
|
+
const description = field.description !== undefined
|
|
56
|
+
? truncateUtf8(field.description, INTERACTION_LIMITS.descriptionBytes)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (field.type === "text") {
|
|
59
|
+
fields.push({
|
|
60
|
+
id: field.id,
|
|
61
|
+
type: "text",
|
|
62
|
+
label,
|
|
63
|
+
...(description !== undefined ? { description } : {}),
|
|
64
|
+
...(field.required !== undefined ? { required: field.required } : {}),
|
|
65
|
+
...(field.secret !== undefined ? { secret: field.secret } : {}),
|
|
66
|
+
...(field.multiline !== undefined ? { multiline: field.multiline } : {}),
|
|
67
|
+
...(field.placeholder !== undefined
|
|
68
|
+
? { placeholder: truncateUtf8(field.placeholder, INTERACTION_LIMITS.labelBytes) }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const options = [];
|
|
74
|
+
const seenOptions = new Set();
|
|
75
|
+
for (const option of field.options.slice(0, INTERACTION_LIMITS.optionsPerField)) {
|
|
76
|
+
if (!validId(option.value) || seenOptions.has(option.value)) {
|
|
77
|
+
return { ok: false, reason: `interaction field has an invalid option id: ${field.id}` };
|
|
78
|
+
}
|
|
79
|
+
seenOptions.add(option.value);
|
|
80
|
+
options.push({
|
|
81
|
+
...option,
|
|
82
|
+
label: truncateUtf8(option.label, INTERACTION_LIMITS.labelBytes),
|
|
83
|
+
...(option.description !== undefined
|
|
84
|
+
? { description: truncateUtf8(option.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
85
|
+
: {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
fields.push({
|
|
89
|
+
id: field.id,
|
|
90
|
+
type: "select",
|
|
91
|
+
label,
|
|
92
|
+
...(description !== undefined ? { description } : {}),
|
|
93
|
+
...(field.required !== undefined ? { required: field.required } : {}),
|
|
94
|
+
...(field.multiple !== undefined ? { multiple: field.multiple } : {}),
|
|
95
|
+
...(field.allowOther !== undefined ? { allowOther: field.allowOther } : {}),
|
|
96
|
+
options,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const seenActions = new Set();
|
|
100
|
+
const actions = [];
|
|
101
|
+
for (const action of request.actions) {
|
|
102
|
+
if (!validId(action.id) || seenActions.has(action.id)) {
|
|
103
|
+
return { ok: false, reason: "interaction has an invalid or duplicate action id" };
|
|
104
|
+
}
|
|
105
|
+
seenActions.add(action.id);
|
|
106
|
+
actions.push({
|
|
107
|
+
...action,
|
|
108
|
+
label: truncateUtf8(action.label, INTERACTION_LIMITS.labelBytes),
|
|
109
|
+
...(action.description !== undefined
|
|
110
|
+
? { description: truncateUtf8(action.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
111
|
+
: {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const context = request.context
|
|
115
|
+
? {
|
|
116
|
+
...(request.context.summary !== undefined
|
|
117
|
+
? { summary: truncateUtf8(request.context.summary, INTERACTION_LIMITS.descriptionBytes) }
|
|
118
|
+
: {}),
|
|
119
|
+
...(request.context.toolName !== undefined
|
|
120
|
+
? { toolName: truncateUtf8(request.context.toolName, INTERACTION_LIMITS.labelBytes) }
|
|
121
|
+
: {}),
|
|
122
|
+
...(request.context.command !== undefined
|
|
123
|
+
? { command: truncateUtf8(request.context.command, INTERACTION_LIMITS.contextBytes) }
|
|
124
|
+
: {}),
|
|
125
|
+
...(request.context.cwd !== undefined
|
|
126
|
+
? { cwd: truncateUtf8(request.context.cwd, INTERACTION_LIMITS.contextBytes) }
|
|
127
|
+
: {}),
|
|
128
|
+
...(request.context.diff !== undefined
|
|
129
|
+
? { diff: truncateUtf8(request.context.diff, INTERACTION_LIMITS.contextBytes) }
|
|
130
|
+
: {}),
|
|
131
|
+
}
|
|
132
|
+
: undefined;
|
|
133
|
+
const boundedPresentation = {
|
|
134
|
+
...request,
|
|
135
|
+
title: truncateUtf8(request.title, INTERACTION_LIMITS.titleBytes),
|
|
136
|
+
...(request.description !== undefined
|
|
137
|
+
? { description: truncateUtf8(request.description, INTERACTION_LIMITS.descriptionBytes) }
|
|
138
|
+
: {}),
|
|
139
|
+
fields,
|
|
140
|
+
actions,
|
|
141
|
+
...(context ? { context } : {}),
|
|
142
|
+
};
|
|
143
|
+
const bounded = boundSessionInteractionRouting(boundedPresentation);
|
|
144
|
+
const lostOptionCoverage = boundedPresentation.fields.some((source, index) => source.type === "select" &&
|
|
145
|
+
source.options.length > 0 &&
|
|
146
|
+
bounded.fields[index]?.type === "select" &&
|
|
147
|
+
bounded.fields[index].options.length === 0);
|
|
148
|
+
if (lostOptionCoverage) {
|
|
149
|
+
return { ok: false, reason: "interaction routing minimum coverage exceeds the canonical budget" };
|
|
150
|
+
}
|
|
151
|
+
if (sessionInteractionRoutingSkeletonBytes(bounded) > INTERACTION_LIMITS.routingSkeletonBytes) {
|
|
152
|
+
return { ok: false, reason: "interaction routing structure exceeds the canonical budget" };
|
|
153
|
+
}
|
|
154
|
+
if (Buffer.byteLength(JSON.stringify(bounded), "utf8") > INTERACTION_LIMITS.requestBytes) {
|
|
155
|
+
return { ok: false, reason: "interaction exceeds the canonical request budget" };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, request: bounded };
|
|
158
|
+
}
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
/** Validate a provider-neutral resolution against the exact advertised form.
|
|
163
|
+
* Unknown keys and wrong answer shapes are rejected even for deny/cancel; the
|
|
164
|
+
* selected action's `requiresAnswers` controls only whether required fields may
|
|
165
|
+
* be omitted. */
|
|
166
|
+
export function validateInteractionResolution(request, resolution) {
|
|
167
|
+
const action = request.actions.find((candidate) => candidate.id === resolution.actionId);
|
|
168
|
+
if (!action)
|
|
169
|
+
return `unknown interaction action: ${resolution.actionId}`;
|
|
170
|
+
const rawAnswers = resolution.answers;
|
|
171
|
+
if (rawAnswers !== undefined && !isRecord(rawAnswers)) {
|
|
172
|
+
return "interaction answers must be an object";
|
|
173
|
+
}
|
|
174
|
+
const answers = rawAnswers ?? {};
|
|
175
|
+
const fields = new Map(request.fields.map((field) => [field.id, field]));
|
|
176
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
177
|
+
const field = fields.get(id);
|
|
178
|
+
if (!field)
|
|
179
|
+
return `unknown interaction answer: ${id}`;
|
|
180
|
+
if (field.type === "text") {
|
|
181
|
+
if (typeof answer !== "string")
|
|
182
|
+
return `answer must be a string: ${id}`;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const values = field.multiple
|
|
186
|
+
? Array.isArray(answer) && answer.every((value) => typeof value === "string")
|
|
187
|
+
? answer
|
|
188
|
+
: null
|
|
189
|
+
: typeof answer === "string"
|
|
190
|
+
? [answer]
|
|
191
|
+
: null;
|
|
192
|
+
if (!values) {
|
|
193
|
+
return field.multiple
|
|
194
|
+
? `answer must be a string array: ${id}`
|
|
195
|
+
: `answer must be a string: ${id}`;
|
|
196
|
+
}
|
|
197
|
+
const allowed = new Set(field.options.map((option) => option.value));
|
|
198
|
+
for (const value of values) {
|
|
199
|
+
if (allowed.has(value))
|
|
200
|
+
continue;
|
|
201
|
+
if (field.allowOther && value.trim().length > 0)
|
|
202
|
+
continue;
|
|
203
|
+
return `answer is not an allowed option: ${id}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!action.requiresAnswers)
|
|
207
|
+
return undefined;
|
|
208
|
+
for (const field of request.fields) {
|
|
209
|
+
if (!field.required)
|
|
210
|
+
continue;
|
|
211
|
+
const answer = answers[field.id];
|
|
212
|
+
const empty = answer === undefined ||
|
|
213
|
+
(typeof answer === "string" && answer.trim().length === 0) ||
|
|
214
|
+
(Array.isArray(answer) &&
|
|
215
|
+
(answer.length === 0 || answer.some((value) => value.trim().length === 0)));
|
|
216
|
+
if (empty)
|
|
217
|
+
return `answer is required: ${field.id}`;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
/** Secret values reach the native runtime but never enter canonical history. */
|
|
222
|
+
export function redactInteractionResolution(request, resolution) {
|
|
223
|
+
if (!resolution.answers)
|
|
224
|
+
return resolution;
|
|
225
|
+
const secretIds = new Set(request.fields
|
|
226
|
+
.filter((field) => field.type === "text" && field.secret)
|
|
227
|
+
.map((field) => field.id));
|
|
228
|
+
if (secretIds.size === 0)
|
|
229
|
+
return resolution;
|
|
230
|
+
const answers = { ...resolution.answers };
|
|
231
|
+
for (const id of secretIds) {
|
|
232
|
+
if (answers[id] !== undefined)
|
|
233
|
+
answers[id] = "[redacted]";
|
|
234
|
+
}
|
|
235
|
+
return { ...resolution, answers };
|
|
236
|
+
}
|
package/dist/models-catalog.d.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend-free model listing for codex/traex.
|
|
3
|
-
*
|
|
4
|
-
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
-
* a live `model/list` RPC anymore. Following omnigent's static-catalog model, we
|
|
6
|
-
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
|
-
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
|
-
* minimal — a fuller curated catalogue can be added here later without touching
|
|
9
|
-
* any caller. claude already has its own static list ({@link listClaudeModels}).
|
|
10
|
-
*/
|
|
11
1
|
import { type AgentRuntimeId, type AppConfig } from "@rynx-ai/core";
|
|
12
2
|
import type { ModelListResponse } from "./codex-app-server/protocol.js";
|
|
3
|
+
export interface RuntimeModelCatalogDeps {
|
|
4
|
+
readTraexModels?: () => Promise<unknown>;
|
|
5
|
+
}
|
|
13
6
|
/**
|
|
14
7
|
* The model list for a runtime, without an execution backend.
|
|
15
|
-
*
|
|
16
|
-
* no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
|
|
8
|
+
* Falls back to the configured model if live Traex discovery is unavailable.
|
|
17
9
|
*/
|
|
18
|
-
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId): ModelListResponse | null
|
|
10
|
+
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId, deps?: RuntimeModelCatalogDeps): Promise<ModelListResponse | null>;
|
package/dist/models-catalog.js
CHANGED
|
@@ -2,26 +2,77 @@
|
|
|
2
2
|
* Backend-free model listing for codex/traex.
|
|
3
3
|
*
|
|
4
4
|
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
-
* a live `model/list` RPC anymore. Following
|
|
6
|
-
* serve a config-derived list
|
|
7
|
-
*
|
|
8
|
-
* minimal — a fuller curated catalogue can be added here later without touching
|
|
9
|
-
* any caller. claude already has its own static list ({@link listClaudeModels}).
|
|
5
|
+
* a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
|
|
6
|
+
* serve a config-derived Codex list and Traex's native `models --json` catalog.
|
|
7
|
+
* claude already has its own static list ({@link listClaudeModels}).
|
|
10
8
|
*/
|
|
11
|
-
import {
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { resolveRuntimeBinary, resolveRuntimeModel, } from "@rynx-ai/core";
|
|
12
12
|
import { listClaudeModels } from "./claude/models.js";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
const TRAEX_MODELS_TIMEOUT_MS = 8_000;
|
|
15
|
+
const TRAEX_MODELS_MAX_BYTES = 2 * 1024 * 1024;
|
|
13
16
|
/**
|
|
14
17
|
* The model list for a runtime, without an execution backend.
|
|
15
|
-
*
|
|
16
|
-
* no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
|
|
18
|
+
* Falls back to the configured model if live Traex discovery is unavailable.
|
|
17
19
|
*/
|
|
18
|
-
export function listRuntimeModels(config, runtime) {
|
|
20
|
+
export async function listRuntimeModels(config, runtime, deps = {}) {
|
|
19
21
|
if (runtime === "claude") {
|
|
20
22
|
return listClaudeModels();
|
|
21
23
|
}
|
|
24
|
+
if (runtime === "traex") {
|
|
25
|
+
try {
|
|
26
|
+
const value = await (deps.readTraexModels ?? readTraexModels)();
|
|
27
|
+
const models = normalizeTraexModels(value, resolveRuntimeModel(config, runtime).trim());
|
|
28
|
+
if (models.length > 0)
|
|
29
|
+
return { data: models };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Keep the configured fallback usable when Traex is missing/logged out.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
const model = resolveRuntimeModel(config, runtime).trim();
|
|
23
36
|
if (!model) {
|
|
24
37
|
return null;
|
|
25
38
|
}
|
|
26
39
|
return { data: [{ id: model, model, isDefault: true }] };
|
|
27
40
|
}
|
|
41
|
+
async function readTraexModels() {
|
|
42
|
+
const { stdout } = await execFileAsync(resolveRuntimeBinary("traex"), ["models", "--json"], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
timeout: TRAEX_MODELS_TIMEOUT_MS,
|
|
45
|
+
maxBuffer: TRAEX_MODELS_MAX_BYTES,
|
|
46
|
+
});
|
|
47
|
+
return JSON.parse(stdout);
|
|
48
|
+
}
|
|
49
|
+
function normalizeTraexModels(value, configuredDefault) {
|
|
50
|
+
if (!Array.isArray(value))
|
|
51
|
+
return [];
|
|
52
|
+
const models = [];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
for (const item of value) {
|
|
55
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
56
|
+
continue;
|
|
57
|
+
const record = item;
|
|
58
|
+
const id = typeof record.name === "string" ? record.name.trim() : "";
|
|
59
|
+
if (!id || seen.has(id))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(id);
|
|
62
|
+
models.push({
|
|
63
|
+
id,
|
|
64
|
+
model: id,
|
|
65
|
+
...(typeof record.description === "string" ? { description: record.description } : {}),
|
|
66
|
+
isDefault: configuredDefault ? id === configuredDefault : models.length === 0,
|
|
67
|
+
...(typeof record.context_window === "number" ? { contextWindow: record.context_window } : {}),
|
|
68
|
+
...(Array.isArray(record.supported_mime_types)
|
|
69
|
+
? { supportedMimeTypes: record.supported_mime_types }
|
|
70
|
+
: {}),
|
|
71
|
+
...(record._meta && typeof record._meta === "object" ? { _meta: record._meta } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (configuredDefault && !seen.has(configuredDefault)) {
|
|
75
|
+
models.unshift({ id: configuredDefault, model: configuredDefault, isDefault: true });
|
|
76
|
+
}
|
|
77
|
+
return models;
|
|
78
|
+
}
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -23,11 +23,16 @@ export declare class RunnerSession {
|
|
|
23
23
|
/** Live terminals hosted by this session, and per-attach client handles. */
|
|
24
24
|
private readonly terminals;
|
|
25
25
|
private readonly attachments;
|
|
26
|
+
/** Opens are async; a close received before attach resolves tombstones the id. */
|
|
27
|
+
private readonly pendingTerminalOpens;
|
|
28
|
+
private readonly cancelledTerminalOpens;
|
|
26
29
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
27
30
|
private readonly liveIds;
|
|
31
|
+
private shuttingDown;
|
|
28
32
|
constructor({ transport, executor, onShutdown }: RunnerSessionDeps);
|
|
29
33
|
private handle;
|
|
30
34
|
private get liveProvider();
|
|
35
|
+
private resolveInteraction;
|
|
31
36
|
/** The mirror channel for a session: an `emit` that forwards every canonical
|
|
32
37
|
* event to the parent under the CURRENT session id (mutable), plus a `retarget`
|
|
33
38
|
* the host calls on a `/clear`·`/fork` rotation — it re-points the mirror to the
|
|
@@ -40,7 +45,7 @@ export declare class RunnerSession {
|
|
|
40
45
|
* launch the detached `codex --remote` TUI, which CREATES the codex thread. The
|
|
41
46
|
* forwarder's connection sees the TUI's broadcast `thread/started`, binds, and
|
|
42
47
|
* subscribes — so the TUI is usable immediately (a fresh `--remote` needs no
|
|
43
|
-
* rollout) and its turns mirror to chat (
|
|
48
|
+
* rollout) and its turns mirror to chat (reference implementation's model).
|
|
44
49
|
*/
|
|
45
50
|
private ensureLive;
|
|
46
51
|
private inject;
|
|
@@ -50,6 +55,9 @@ export declare class RunnerSession {
|
|
|
50
55
|
* so the web attach reuses the same detached pane. */
|
|
51
56
|
private launchCodexPane;
|
|
52
57
|
private stopLive;
|
|
58
|
+
/** Stop event forwarding, kill native terminals/hooks, then synchronously
|
|
59
|
+
* scrub provider handoff files before the child process is allowed to exit. */
|
|
60
|
+
shutdown(): void;
|
|
53
61
|
private openTerminal;
|
|
54
62
|
/** Attach an already-created terminal and forward its data/exit to the parent. */
|
|
55
63
|
private attachExisting;
|