@zq-silk/yui 2.1.0 → 2.2.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/README.md +4 -0
- package/dist/cli/commandCatalog.js +24 -3
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +79 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +6 -1
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +176 -74
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/demo.js +12 -1
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +13 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +270 -133
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +2 -2
- package/dist/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/app.js +64 -3
- package/dist/web/assets/client/detail.js +6 -6
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/overview.js +12 -9
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +2 -0
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +12 -4
- package/dist/web/assets/styles/views.js +16 -13
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +47 -9
- package/docs/release-workflow.zh-CN.md +36 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +7 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { open, lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, resolve, parse, relative, sep } from 'node:path';
|
|
3
|
+
import { createLocalToolBinding } from '../index.js';
|
|
4
|
+
export class ProductError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
field;
|
|
7
|
+
nextAction;
|
|
8
|
+
effect;
|
|
9
|
+
sessionId;
|
|
10
|
+
constructor(code, field, nextAction, effect, sessionId) {
|
|
11
|
+
super(`${code}: ${field}; ${nextAction}`);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.field = field;
|
|
14
|
+
this.nextAction = nextAction;
|
|
15
|
+
this.effect = effect;
|
|
16
|
+
this.sessionId = sessionId;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function productFailure(error) {
|
|
20
|
+
// Never format arbitrary thrown messages, causes, argv or HTTP response bodies.
|
|
21
|
+
return error instanceof ProductError
|
|
22
|
+
? { code: error.code, field: error.field, nextAction: error.nextAction,
|
|
23
|
+
...(error.effect ? { effect: error.effect } : {}),
|
|
24
|
+
...(error.sessionId ? { sessionId: error.sessionId } : {}) }
|
|
25
|
+
: { code: 'agent_execution', field: 'runtime', nextAction: 'Inspect saved facts; do not replay uncertain effects.' };
|
|
26
|
+
}
|
|
27
|
+
const invalid = (field, action = 'Provide a supported explicit value and restart.') => {
|
|
28
|
+
throw new ProductError('agent_config', field, action);
|
|
29
|
+
};
|
|
30
|
+
const fields = ['adapter', 'endpoint', 'model', 'credentialRef', 'root', 'cwd', 'stateDir', 'tools',
|
|
31
|
+
'maxSteps', 'contextBytes', 'outputReserveBytes', 'modelTimeoutMs', 'stream', 'command'];
|
|
32
|
+
const options = {
|
|
33
|
+
'--adapter': 'adapter', '--endpoint': 'endpoint', '--model': 'model', '--credential-ref': 'credentialRef',
|
|
34
|
+
'--root': 'root', '--cwd': 'cwd', '--state-dir': 'stateDir', '--tools': 'tools', '--max-steps': 'maxSteps',
|
|
35
|
+
'--context-bytes': 'contextBytes', '--output-reserve-bytes': 'outputReserveBytes',
|
|
36
|
+
'--model-timeout-ms': 'modelTimeoutMs', '--stream': 'stream', '--command-config': 'command',
|
|
37
|
+
};
|
|
38
|
+
export const productConfigurationOptions = [...Object.keys(options), '--config',
|
|
39
|
+
'--allow-write', '--allow-command', '--allow-memory-write', '--allow-http'];
|
|
40
|
+
const environment = {
|
|
41
|
+
adapter: 'NATIVE_AGENT_ADAPTER', endpoint: 'NATIVE_AGENT_ENDPOINT', model: 'NATIVE_AGENT_MODEL',
|
|
42
|
+
credentialRef: 'NATIVE_AGENT_CREDENTIAL_REF', root: 'NATIVE_AGENT_ROOT', cwd: 'NATIVE_AGENT_CWD', stateDir: 'NATIVE_AGENT_STATE_DIR',
|
|
43
|
+
tools: 'NATIVE_AGENT_TOOLS', maxSteps: 'NATIVE_AGENT_MAX_STEPS', contextBytes: 'NATIVE_AGENT_CONTEXT_BYTES',
|
|
44
|
+
outputReserveBytes: 'NATIVE_AGENT_OUTPUT_RESERVE_BYTES', modelTimeoutMs: 'NATIVE_AGENT_MODEL_TIMEOUT_MS',
|
|
45
|
+
stream: 'NATIVE_AGENT_STREAM', command: 'NATIVE_AGENT_COMMAND_CONFIG',
|
|
46
|
+
};
|
|
47
|
+
const toolNames = ['read', 'list', 'find', 'search', 'write', 'edit', 'command', 'project_context', 'project_memory'];
|
|
48
|
+
function text(value, field, max = 4096) {
|
|
49
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/u.test(value))
|
|
50
|
+
return invalid(field);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function integer(value, field, minimum, maximum) {
|
|
54
|
+
const number = typeof value === 'string' && /^\d+$/u.test(value) ? Number(value) : value;
|
|
55
|
+
if (typeof number !== 'number' || !Number.isSafeInteger(number) || number < minimum || number > maximum)
|
|
56
|
+
return invalid(field);
|
|
57
|
+
return number;
|
|
58
|
+
}
|
|
59
|
+
async function configurationFile(path) {
|
|
60
|
+
let handle;
|
|
61
|
+
try {
|
|
62
|
+
handle = await open(path, 'r');
|
|
63
|
+
if (!(await handle.stat()).isFile())
|
|
64
|
+
return invalid('config');
|
|
65
|
+
const buffer = Buffer.alloc(65537);
|
|
66
|
+
let length = 0;
|
|
67
|
+
while (length < buffer.length) {
|
|
68
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
69
|
+
if (!bytesRead)
|
|
70
|
+
break;
|
|
71
|
+
length += bytesRead;
|
|
72
|
+
}
|
|
73
|
+
if (length > 65536)
|
|
74
|
+
return invalid('config', 'Use a JSON configuration no larger than 64 KiB.');
|
|
75
|
+
const object = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, length)));
|
|
76
|
+
if (!object || typeof object !== 'object' || Array.isArray(object) || object.schemaVersion !== 1
|
|
77
|
+
|| Object.keys(object).some(key => key !== 'schemaVersion' && !fields.includes(key)))
|
|
78
|
+
return invalid('config');
|
|
79
|
+
return object;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return invalid('config', 'Select a readable, valid version-1 JSON configuration.');
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await handle?.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Data-only explicit file; no discovery, scripts, account lookup or Yui configuration. */
|
|
89
|
+
export async function resolveProductArguments(args, env, launchCwd) {
|
|
90
|
+
const [command, ...tail] = args;
|
|
91
|
+
if (!['check-config', 'start', 'run'].includes(command))
|
|
92
|
+
return invalid('command', 'Use check-config, start or run; see yui agent --help.');
|
|
93
|
+
const cli = {};
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
let filePath, session, input;
|
|
96
|
+
let allowWrite = false, allowCommand = false, allowMemoryWrite = false, allowHttp = false;
|
|
97
|
+
for (let index = 0; index < tail.length; index++) {
|
|
98
|
+
const key = tail[index];
|
|
99
|
+
if (seen.has(key))
|
|
100
|
+
return invalid('arguments', 'Do not repeat options.');
|
|
101
|
+
seen.add(key);
|
|
102
|
+
if (key === '--json')
|
|
103
|
+
continue;
|
|
104
|
+
if (key === '--allow-write') {
|
|
105
|
+
allowWrite = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (key === '--allow-command') {
|
|
109
|
+
allowCommand = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (key === '--allow-memory-write') {
|
|
113
|
+
allowMemoryWrite = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (key === '--allow-http') {
|
|
117
|
+
allowHttp = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!Object.hasOwn(options, key) && !['--config', '--session', '--input'].includes(key))
|
|
121
|
+
return invalid('arguments');
|
|
122
|
+
const value = tail[++index];
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
return invalid('arguments');
|
|
125
|
+
if (key === '--config')
|
|
126
|
+
filePath = resolve(launchCwd, text(value, 'config'));
|
|
127
|
+
else if (key === '--session')
|
|
128
|
+
session = text(value, 'session', 128);
|
|
129
|
+
else if (key === '--input') {
|
|
130
|
+
if (!value.trim() || Buffer.byteLength(value) > 65536)
|
|
131
|
+
return invalid('input');
|
|
132
|
+
input = value;
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
cli[options[key]] = value;
|
|
136
|
+
}
|
|
137
|
+
if (command === 'start' && seen.has('--json'))
|
|
138
|
+
return invalid('arguments', 'Use run for JSON results; start is a line UI.');
|
|
139
|
+
if ((command === 'run') !== (input !== undefined))
|
|
140
|
+
return invalid('input', 'run requires --input; start/check-config do not accept it.');
|
|
141
|
+
if (session !== undefined && !/^[A-Za-z0-9_-]{1,128}$/u.test(session))
|
|
142
|
+
return invalid('session');
|
|
143
|
+
if (command === 'check-config' && session !== undefined)
|
|
144
|
+
return invalid('session');
|
|
145
|
+
const file = filePath ? await configurationFile(filePath) : {};
|
|
146
|
+
const defaults = { adapter: 'chat-completions', cwd: launchCwd,
|
|
147
|
+
tools: ['read', 'list', 'find', 'search', 'project_context', 'project_memory'], maxSteps: 8, contextBytes: 1048576,
|
|
148
|
+
outputReserveBytes: 0, modelTimeoutMs: 30000, stream: false };
|
|
149
|
+
const values = {};
|
|
150
|
+
const sources = {};
|
|
151
|
+
for (const field of fields) {
|
|
152
|
+
const environmentValue = env[environment[field]];
|
|
153
|
+
sources[field] = cli[field] !== undefined ? 'cli' : environmentValue !== undefined ? 'environment'
|
|
154
|
+
: file[field] !== undefined ? 'file' : 'default';
|
|
155
|
+
values[field] = sources[field] === 'cli' ? cli[field] : sources[field] === 'environment' ? environmentValue
|
|
156
|
+
: sources[field] === 'file' ? file[field] : defaults[field];
|
|
157
|
+
}
|
|
158
|
+
if (values.adapter !== 'chat-completions')
|
|
159
|
+
return invalid('adapter', 'Only chat-completions is supported.');
|
|
160
|
+
const endpoint = text(values.endpoint, 'endpoint');
|
|
161
|
+
try {
|
|
162
|
+
const url = new URL(endpoint);
|
|
163
|
+
if (url.username || url.password || url.search || url.hash
|
|
164
|
+
|| !(url.protocol === 'https:' || (allowHttp && url.protocol === 'http:'
|
|
165
|
+
&& ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))))
|
|
166
|
+
return invalid('endpoint');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return invalid('endpoint', 'Use an exact HTTPS URL without userinfo/query/fragment, or explicit --allow-http for loopback fixtures.');
|
|
170
|
+
}
|
|
171
|
+
const credentialRef = text(values.credentialRef, 'credentialRef', 256);
|
|
172
|
+
if (credentialRef !== 'anonymous' && !/^env:[A-Za-z_][A-Za-z0-9_]*$/u.test(credentialRef))
|
|
173
|
+
return invalid('credentialRef', 'Use env:VARIABLE_NAME or explicit anonymous.');
|
|
174
|
+
const path = (field) => {
|
|
175
|
+
const value = text(values[field], field);
|
|
176
|
+
if (sources[field] === 'environment' && !isAbsolute(value))
|
|
177
|
+
return invalid(field, 'Environment paths must be absolute.');
|
|
178
|
+
return resolve(sources[field] === 'file' ? dirname(filePath) : launchCwd, value);
|
|
179
|
+
};
|
|
180
|
+
// Keep explicit provenance. On resume, absent paths must be replaced by the
|
|
181
|
+
// stored location BEFORE any filesystem admission (not launch-cwd defaults).
|
|
182
|
+
const cwd = path('cwd');
|
|
183
|
+
const root = sources.root === 'default' ? cwd : path('root');
|
|
184
|
+
if (!session)
|
|
185
|
+
await validateProductLocation({ root, cwd });
|
|
186
|
+
const stateDir = path('stateDir');
|
|
187
|
+
const selected = typeof values.tools === 'string' ? values.tools.split(',') : values.tools;
|
|
188
|
+
if (!Array.isArray(selected) || selected.length > toolNames.length
|
|
189
|
+
|| selected.some(tool => !toolNames.includes(tool)) || new Set(selected).size !== selected.length)
|
|
190
|
+
return invalid('tools');
|
|
191
|
+
if (selected.some(tool => ['write', 'edit'].includes(tool)) && !allowWrite)
|
|
192
|
+
return invalid('tools', 'Pass --allow-write explicitly on this invocation to select write/edit.');
|
|
193
|
+
if (selected.includes('command') && !allowCommand)
|
|
194
|
+
return invalid('tools', 'Pass --allow-command explicitly on this invocation to select command; this is not a sandbox.');
|
|
195
|
+
const stream = values.stream === 'true' ? true : values.stream === 'false' ? false : values.stream;
|
|
196
|
+
if (typeof stream !== 'boolean')
|
|
197
|
+
return invalid('stream');
|
|
198
|
+
const contextBytes = integer(values.contextBytes, 'contextBytes', 1, 1048576);
|
|
199
|
+
const outputReserveBytes = integer(values.outputReserveBytes, 'outputReserveBytes', 0, contextBytes - 1);
|
|
200
|
+
let reviewedCommand = values.command;
|
|
201
|
+
if (typeof reviewedCommand === 'string') {
|
|
202
|
+
try {
|
|
203
|
+
if (Buffer.byteLength(reviewedCommand) > 65536)
|
|
204
|
+
return invalid('command');
|
|
205
|
+
reviewedCommand = JSON.parse(reviewedCommand);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return invalid('command', 'Provide a bounded JSON object containing explicit env and reviewed exact specs.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (reviewedCommand !== undefined && (!reviewedCommand || typeof reviewedCommand !== 'object'
|
|
212
|
+
|| Array.isArray(reviewedCommand)))
|
|
213
|
+
return invalid('command');
|
|
214
|
+
return {
|
|
215
|
+
command: command, allowWrite, allowMemoryWrite,
|
|
216
|
+
...(session ? { session } : {}), ...(input !== undefined ? { input } : {}),
|
|
217
|
+
config: { adapter: 'chat-completions', endpoint, model: text(values.model, 'model', 256), credentialRef,
|
|
218
|
+
root, cwd, stateDir, tools: [...selected], sources, stream, contextBytes, outputReserveBytes,
|
|
219
|
+
...(reviewedCommand !== undefined ? { command: reviewedCommand } : {}),
|
|
220
|
+
maxSteps: integer(values.maxSteps, 'maxSteps', 1, 100),
|
|
221
|
+
modelTimeoutMs: integer(values.modelTimeoutMs, 'modelTimeoutMs', 1, 300000) },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Existence and symlink checks belong to this consumer, not stored metadata.
|
|
225
|
+
* Controlled local directories only; this is not a hostile-race OS sandbox. */
|
|
226
|
+
export async function validateProductLocation(location) {
|
|
227
|
+
for (const field of ['root', 'cwd']) {
|
|
228
|
+
const value = location[field];
|
|
229
|
+
try {
|
|
230
|
+
if (!isAbsolute(value) || resolve(value) !== value || Buffer.byteLength(value) > 4096
|
|
231
|
+
|| /[\p{Cc}\p{Cs}]/u.test(value))
|
|
232
|
+
throw new Error();
|
|
233
|
+
let current = value;
|
|
234
|
+
for (;;) {
|
|
235
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
236
|
+
throw new Error();
|
|
237
|
+
const parent = dirname(current);
|
|
238
|
+
if (current === parse(current).root)
|
|
239
|
+
break;
|
|
240
|
+
current = parent;
|
|
241
|
+
}
|
|
242
|
+
if (!(await lstat(value)).isDirectory() || await realpath(value) !== value)
|
|
243
|
+
throw new Error();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return invalid(field, 'Select an existing canonical controlled directory without symlinks.');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const within = relative(location.root, location.cwd);
|
|
250
|
+
if (within === '..' || within.startsWith(`..${sep}`) || isAbsolute(within))
|
|
251
|
+
return invalid('cwd', 'cwd must be inside the explicitly selected root.');
|
|
252
|
+
}
|
|
253
|
+
/** One real producer factory owns tools, permission and live environments.
|
|
254
|
+
* The selected data is not authority until current invocation opt-ins admit it. */
|
|
255
|
+
export function createProductBinding(invocation, credential) {
|
|
256
|
+
try {
|
|
257
|
+
const { config } = invocation;
|
|
258
|
+
return createLocalToolBinding({
|
|
259
|
+
root: config.root, cwd: config.cwd, allowWrite: invocation.allowWrite,
|
|
260
|
+
allowCommand: config.tools.includes('command'), command: config.command,
|
|
261
|
+
...(credential ? { redact: [credential] } : {}),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return invalid('binding', 'Select a canonical controlled directory and valid reviewed command executable/argv/effect/env; settle old execution before rebuilding.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Specs/argv and environment values are caller data, not public diagnostics. */
|
|
269
|
+
export function publicConfiguration(config) {
|
|
270
|
+
const { command: _command, ...configuration } = config;
|
|
271
|
+
return configuration;
|
|
272
|
+
}
|
|
273
|
+
/** Resolved only in memory. Never returned in configuration, receipts or diagnostics. */
|
|
274
|
+
export function resolveCredential(config, env) {
|
|
275
|
+
const token = resolveCredentialReference(config.credentialRef, env);
|
|
276
|
+
if (token && containsCredential(config, token))
|
|
277
|
+
return invalid('credentialRef', 'Keep the credential value separate from public configuration.');
|
|
278
|
+
return token;
|
|
279
|
+
}
|
|
280
|
+
export function resolveCredentialReference(ref, env) {
|
|
281
|
+
if (ref === 'anonymous')
|
|
282
|
+
return undefined;
|
|
283
|
+
if (!/^env:[A-Za-z_][A-Za-z0-9_]*$/u.test(ref))
|
|
284
|
+
return invalid('credentialRef');
|
|
285
|
+
const token = env[ref.slice(4)];
|
|
286
|
+
if (!token?.trim() || token.length > 4096 || /[\x00-\x1f\x7f]/u.test(token))
|
|
287
|
+
return invalid('credentialRef', 'Set the referenced credential environment variable to a valid nonempty value.');
|
|
288
|
+
return token;
|
|
289
|
+
}
|
|
290
|
+
/** Exact configured secret protection, not heuristic detection of arbitrary user secrets. */
|
|
291
|
+
export function containsCredential(value, token) {
|
|
292
|
+
return typeof value === 'string' ? value.includes(token)
|
|
293
|
+
: value !== null && typeof value === 'object' && Object.entries(value)
|
|
294
|
+
.some(([key, child]) => key.includes(token) || containsCredential(child, token));
|
|
295
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { containsCredential, ProductError } from './config.js';
|
|
3
|
+
/** Only validated current-invocation facts. No env values, credentials, account,
|
|
4
|
+
* persisted grants or command authority tokens. The real 82 check still rules. */
|
|
5
|
+
export function createRuntimeFacts(invocation, binding, sessionId, credential) {
|
|
6
|
+
const facts = {
|
|
7
|
+
type: 'runtime-facts', sessionId, root: binding.root, cwd: binding.cwd,
|
|
8
|
+
tools: [...invocation.config.tools],
|
|
9
|
+
grants: { allowWrite: binding.allowWrite, allowCommand: binding.allowCommand,
|
|
10
|
+
allowMemoryWrite: invocation.allowMemoryWrite },
|
|
11
|
+
commands: binding.allowCommand ? (invocation.config.command?.specs ?? [])
|
|
12
|
+
.filter(spec => spec.effect === 'read' || binding.allowWrite)
|
|
13
|
+
.map(spec => ({ executable: spec.executable, argv: [...spec.argv], effect: spec.effect })) : [],
|
|
14
|
+
};
|
|
15
|
+
if (credential && containsCredential(facts, credential))
|
|
16
|
+
throw new ProductError('agent_config', 'runtimeFacts', 'Keep the configured credential separate from runtime facts.');
|
|
17
|
+
const content = JSON.stringify(facts);
|
|
18
|
+
const revision = createHash('sha256').update(content).digest('hex');
|
|
19
|
+
return {
|
|
20
|
+
id: 'product-runtime',
|
|
21
|
+
async load(scope, signal) {
|
|
22
|
+
if (signal.aborted || scope.sessionId !== sessionId)
|
|
23
|
+
throw new Error('Runtime fact scope unavailable');
|
|
24
|
+
// Data -> user material: configuration arguments never become system
|
|
25
|
+
// instructions. Required budget admission cannot silently drop these facts.
|
|
26
|
+
return [{ id: 'current-invocation', kind: 'data', source: 'product-runtime',
|
|
27
|
+
content, revision, required: true }];
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { runProductRuntime } from './runtime.js';
|
|
2
|
+
import { ProductError, resolveCredential, resolveProductArguments, createProductBinding, publicConfiguration } from './config.js';
|
|
3
|
+
import { catalogCommands, runCatalogCommand } from './catalog.js';
|
|
4
|
+
export { productFailure } from './config.js';
|
|
5
|
+
export const productHelp = `Independent coding Agent (no Controller or Yui Home)
|
|
6
|
+
yui agent check-config [options]
|
|
7
|
+
yui agent start [options] [--session ID]
|
|
8
|
+
yui agent run [options] --input TEXT [--session ID]
|
|
9
|
+
yui agent sessions --state-dir PATH [--limit N] [--cursor CURSOR]
|
|
10
|
+
yui agent session-info|history --state-dir PATH --session ID [history: --limit N --cursor CURSOR]
|
|
11
|
+
yui agent rename --state-dir PATH --session ID --title JSON_STRING_OR_NULL --expected-metadata-revision N
|
|
12
|
+
|
|
13
|
+
Required: --endpoint COMPLETE_URL --model NAME --credential-ref env:NAME|anonymous --state-dir PATH
|
|
14
|
+
Optional: --config FILE --root PATH --cwd PATH --adapter chat-completions
|
|
15
|
+
--tools read,list,find,search,project_context,project_memory --max-steps 8
|
|
16
|
+
--context-bytes 1048576 --output-reserve-bytes 0
|
|
17
|
+
--model-timeout-ms 30000 --stream true|false
|
|
18
|
+
Explicit opt-ins on EVERY invocation: --allow-write --allow-command --allow-memory-write --allow-http
|
|
19
|
+
Only loopback HTTP is allowed. --command-config JSON supplies explicit env and reviewed specs.
|
|
20
|
+
Command authorization is exact executable/argv/cwd/effect; no arbitrary model scripts.
|
|
21
|
+
Complete env permits only PATH/LANG/LC_ALL/TZ; nothing is inherited.
|
|
22
|
+
start uses the existing line UI: text, /cancel, /sessions [CURSOR], /history [CURSOR], /info,
|
|
23
|
+
/rename EXPECTED_METADATA_REVISION JSON_STRING_OR_NULL, /use ID, /new, /quit.
|
|
24
|
+
Ctrl-C cancels the active turn (or exits when idle); SIGTERM/EOF close owned execution.
|
|
25
|
+
run emits result + exact save receipt; exit 0 completed, 1 error, 3 budget, 130 cancelled.
|
|
26
|
+
CLI > NATIVE_AGENT_* environment > explicit version-1 JSON file > safe defaults.
|
|
27
|
+
No automatic config discovery, account fallback, tool replay or inherited grants.
|
|
28
|
+
Real local tools/permission/environment are created together; reopen rebuilds current authority.
|
|
29
|
+
Project guidance is required; Skills load completely on request as project/user data.
|
|
30
|
+
project_memory reads .agents/MEMORY.md; replace/delete need --allow-memory-write, not --allow-write.
|
|
31
|
+
Catalog commands need only the existing state directory; reads never start execution.
|
|
32
|
+
Duplicate titles are not identities. Stale cursors require an explicit first-page refresh.
|
|
33
|
+
New Sessions atomically persist root/cwd (root defaults to cwd).
|
|
34
|
+
Resume uses the saved location; explicit conflicts and legacy missing locations are refused.
|
|
35
|
+
Paths must exist without symlinks, with cwd inside root. Saved paths grant no authority.
|
|
36
|
+
`;
|
|
37
|
+
export async function runProductCommand(args) {
|
|
38
|
+
const helpArguments = args.filter(argument => argument !== '--json');
|
|
39
|
+
if (helpArguments.length === 0 || (helpArguments.length === 1 && ['help', '--help', '-h'].includes(helpArguments[0]))) {
|
|
40
|
+
process.stdout.write(args.includes('--json') ? `${JSON.stringify({ help: productHelp })}\n` : productHelp);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (catalogCommands.some(command => command === args[0]))
|
|
44
|
+
return runCatalogCommand(args, process.env, process.cwd());
|
|
45
|
+
const invocation = await resolveProductArguments(args, process.env, process.cwd());
|
|
46
|
+
const credential = resolveCredential(invocation.config, process.env);
|
|
47
|
+
if (credential && invocation.input?.includes(credential)) {
|
|
48
|
+
throw new ProductError('agent_config', 'input', 'Do not submit the configured credential as conversation text.');
|
|
49
|
+
}
|
|
50
|
+
if (credential && invocation.session?.includes(credential)) {
|
|
51
|
+
throw new ProductError('agent_config', 'session', 'Do not use the configured credential as a Session identity.');
|
|
52
|
+
}
|
|
53
|
+
if (invocation.command === 'check-config') {
|
|
54
|
+
const binding = createProductBinding(invocation, credential);
|
|
55
|
+
process.stdout.write(`${JSON.stringify({ configuration: publicConfiguration(invocation.config),
|
|
56
|
+
binding: binding.binding, projectAuthority: { allowMemoryWrite: invocation.allowMemoryWrite },
|
|
57
|
+
credential: 'resolved-in-memory',
|
|
58
|
+
network: 'not-contacted', state: 'not-opened' })}\n`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
return runProductRuntime(invocation, credential);
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SessionSaveError } from '../session/index.js';
|
|
2
|
+
import { ProductError, validateProductLocation } from './config.js';
|
|
3
|
+
import { storageFailure } from './storage.js';
|
|
4
|
+
/** A bounded detail is a location fact, never permission or execution history. */
|
|
5
|
+
export async function restoreProductLocation(store, id, config) {
|
|
6
|
+
let detail;
|
|
7
|
+
try {
|
|
8
|
+
detail = await store.getSessionInfo(id);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw storageFailure(error);
|
|
12
|
+
}
|
|
13
|
+
if (!detail.location)
|
|
14
|
+
throw new ProductError('agent_storage', 'location', 'Saved location is missing; choose a new Session explicitly. Never guess or retrofit the old location.', undefined, id);
|
|
15
|
+
for (const field of ['root', 'cwd']) {
|
|
16
|
+
if (config.sources[field] !== 'default' && config[field] !== detail.location[field])
|
|
17
|
+
throw new ProductError('agent_config', field, 'Explicit location conflicts with this Session; select the original location or a new Session.');
|
|
18
|
+
}
|
|
19
|
+
await validateProductLocation(detail.location);
|
|
20
|
+
return detail.location;
|
|
21
|
+
}
|
|
22
|
+
/** Creation stays in the unique owner. A lost acknowledgment stops this attempt;
|
|
23
|
+
* reconcile reads by the attempted ID, never retry create or run effects here. */
|
|
24
|
+
export async function createProductSession(owner, store, title, location) {
|
|
25
|
+
try {
|
|
26
|
+
return await owner.create(title, location);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (!(error instanceof SessionSaveError) || error.expectedRevision !== null)
|
|
30
|
+
throw storageFailure(error);
|
|
31
|
+
let confirmed = false;
|
|
32
|
+
try {
|
|
33
|
+
const detail = await store.getSessionInfo(error.sessionId);
|
|
34
|
+
const saved = await store.load(error.sessionId);
|
|
35
|
+
confirmed = detail.location?.root === location.root && detail.location.cwd === location.cwd
|
|
36
|
+
&& saved.revision === 0 && detail.revision === 0 && detail.digest === saved.digest
|
|
37
|
+
&& saved.recovery.disposition === 'ready';
|
|
38
|
+
}
|
|
39
|
+
catch { /* Read failure/not-found is not proof that no commit occurred. */ }
|
|
40
|
+
throw new ProductError('agent_storage', 'creation', confirmed
|
|
41
|
+
? `Same-ID detail and load confirm empty Session ${error.sessionId} and original location. No execution started; explicitly reopen this Session ID.`
|
|
42
|
+
: `Creation of Session ${error.sessionId} remains unknown. Preserve this ID and inspect same-ID detail plus load; do not blindly recreate, switch IDs or replay.`, 'unknown', error.sessionId);
|
|
43
|
+
}
|
|
44
|
+
}
|