@steerable/agent-shell 0.6.48 → 0.6.50
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/host/runtime.js +3 -3
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +1 -0
- package/dist/llm/sidecar-provider.d.ts +0 -4
- package/dist/llm/sidecar-provider.js +0 -4
- package/dist/local-backend/coreloop-stream.d.ts +1 -3
- package/dist/local-backend/coreloop-stream.js +1 -3
- package/dist/local-backend/portable-bundle.d.ts +202 -0
- package/dist/local-backend/portable-bundle.js +318 -0
- package/dist/local-backend/portable-service.d.ts +23 -0
- package/dist/local-backend/portable-service.js +469 -0
- package/dist/local-backend/router.js +20 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +2 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +1 -1
- package/dist/local-backend/turn-files.d.ts +20 -0
- package/dist/local-backend/turn-files.js +162 -5
- package/dist/present-files.d.ts +64 -0
- package/dist/present-files.js +136 -0
- package/dist/product-config.d.ts +6 -0
- package/dist/product-config.js +4 -0
- package/dist/sidecar/boot.d.ts +19 -4
- package/dist/sidecar/boot.js +37 -20
- package/dist/sidecar/handle.d.ts +5 -8
- package/dist/sidecar/handle.js +6 -10
- package/dist/sidecar/supervisor.d.ts +0 -9
- package/dist/sidecar/supervisor.js +16 -92
- package/dist/sidecar/types.d.ts +0 -2
- package/dist/tool-router.d.ts +19 -2
- package/dist/tool-router.js +63 -2
- package/package.json +4 -4
|
@@ -25,8 +25,6 @@ const READY_PREFIX = '__SIDECAR_READY__:';
|
|
|
25
25
|
const DEFAULT_BOOT_TIMEOUT_MS = 15_000;
|
|
26
26
|
const DEFAULT_HEALTH_INTERVAL_MS = 5_000;
|
|
27
27
|
const DEFAULT_RESTART_AFTER_FAILED_PINGS = 3;
|
|
28
|
-
export const RUST_SIDECAR_ENV = 'STEERABLE_RUST_SIDECAR';
|
|
29
|
-
export const RUST_SIDECAR_BIN_ENV = 'STEERABLE_RUST_SIDECAR_BIN';
|
|
30
28
|
// Only /usr/bin/sandbox-exec is trusted — a PATH-relative lookup could
|
|
31
29
|
// resolve to an attacker-planted binary (codex's rule).
|
|
32
30
|
const SEATBELT_EXECUTABLE = '/usr/bin/sandbox-exec';
|
|
@@ -384,23 +382,10 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
384
382
|
// Internal: boot
|
|
385
383
|
// ------------------------------------------------------------------
|
|
386
384
|
async boot() {
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let spawnPlan;
|
|
392
|
-
if (rustBin) {
|
|
393
|
-
spawnPlan = await this.resolveSandboxedSpawn(rustBin, this.options.args ?? []);
|
|
394
|
-
}
|
|
395
|
-
else if (useRustSidecar) {
|
|
396
|
-
throw new Error('STEERABLE_RUST_SIDECAR is enabled but the Rust sidecar binary is missing');
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
const py = this.resolvePythonBinary();
|
|
400
|
-
const entry = this.options.entryModule ?? 'steerable_sidecar';
|
|
401
|
-
const args = ['-m', entry, ...(this.options.args ?? [])];
|
|
402
|
-
spawnPlan = await this.resolveSandboxedSpawn(py, args);
|
|
403
|
-
}
|
|
385
|
+
const py = this.resolvePythonBinary();
|
|
386
|
+
const entry = this.options.entryModule ?? 'steerable_sidecar';
|
|
387
|
+
const args = ['-m', entry, ...(this.options.args ?? [])];
|
|
388
|
+
const spawnPlan = await this.resolveSandboxedSpawn(py, args);
|
|
404
389
|
// Profile generation is asynchronous. A quit can begin while it runs;
|
|
405
390
|
// never spawn a new child after shutdown has claimed the supervisor.
|
|
406
391
|
if (this.shuttingDown) {
|
|
@@ -502,12 +487,7 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
502
487
|
...(webEgress ? ['--allow-web-egress'] : []),
|
|
503
488
|
...(allowResolver ? ['--allow-resolver'] : []),
|
|
504
489
|
];
|
|
505
|
-
const
|
|
506
|
-
? resolveRustSidecarBin(this.options.rustSidecarBin)
|
|
507
|
-
: undefined;
|
|
508
|
-
const { stdout } = rustBin
|
|
509
|
-
? await execFileAsync(rustBin, ['sandbox', 'profile', ...flags], { timeout: 10_000 })
|
|
510
|
-
: await execFileAsync(command, ['-m', 'steerable_sidecar.sandbox', 'profile', ...flags], { timeout: 10_000 });
|
|
490
|
+
const { stdout } = await execFileAsync(command, ['-m', 'steerable_sidecar.sandbox', 'profile', ...flags], { timeout: 10_000 });
|
|
511
491
|
const profile = stdout.trim();
|
|
512
492
|
if (!profile.includes('(deny default)')) {
|
|
513
493
|
throw new Error('generated profile is not a Seatbelt policy');
|
|
@@ -552,29 +532,17 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
552
532
|
// 都透传环境)+ sidecar 应用层域名名单强制,网络命名空间保持共享。
|
|
553
533
|
// 这里如实记录,不假装接上了实际不强制的参数。
|
|
554
534
|
const egressViaProxy = this.options.env?.STEERABLE_EGRESS_CONFINED === '1';
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
command,
|
|
565
|
-
...args,
|
|
566
|
-
]
|
|
567
|
-
: [
|
|
568
|
-
'-m',
|
|
569
|
-
'steerable_sidecar.sandbox',
|
|
570
|
-
'linux-wrap',
|
|
571
|
-
...writableRoots.flatMap((root) => ['--writable-root', root]),
|
|
572
|
-
'--',
|
|
573
|
-
command,
|
|
574
|
-
...args,
|
|
575
|
-
];
|
|
535
|
+
const wrapArgs = [
|
|
536
|
+
'-m',
|
|
537
|
+
'steerable_sidecar.sandbox',
|
|
538
|
+
'linux-wrap',
|
|
539
|
+
...writableRoots.flatMap((root) => ['--writable-root', root]),
|
|
540
|
+
'--',
|
|
541
|
+
command,
|
|
542
|
+
...args,
|
|
543
|
+
];
|
|
576
544
|
try {
|
|
577
|
-
const { stdout } = await execFileAsync(
|
|
545
|
+
const { stdout } = await execFileAsync(command, wrapArgs, { timeout: 15_000 });
|
|
578
546
|
const plan = JSON.parse(stdout.trim());
|
|
579
547
|
if (!Array.isArray(plan.argv) || plan.argv.length < 1 || typeof plan.argv[0] !== 'string') {
|
|
580
548
|
throw new Error('linux-wrap did not return an argv');
|
|
@@ -976,49 +944,5 @@ export function resolveSidecarPython(pythonExecutable) {
|
|
|
976
944
|
if (existsSync(candidate))
|
|
977
945
|
return candidate;
|
|
978
946
|
}
|
|
979
|
-
|
|
980
|
-
return process.platform === 'win32' ? 'python' : 'python3';
|
|
981
|
-
}
|
|
982
|
-
function envFlag(name) {
|
|
983
|
-
const raw = (process.env[name] ?? '').trim().toLowerCase();
|
|
984
|
-
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
985
|
-
}
|
|
986
|
-
export function rustSidecarEnabled() {
|
|
987
|
-
if ((process.env[RUST_SIDECAR_ENV] ?? '').trim()) {
|
|
988
|
-
return envFlag(RUST_SIDECAR_ENV);
|
|
989
|
-
}
|
|
990
|
-
return resolveRustSidecarBin() !== undefined;
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Resolve the Rust sidecar binary from an explicit path, a packaged engine,
|
|
994
|
-
* or a framework source checkout. Boot fails when Rust was explicitly
|
|
995
|
-
* enabled but no binary resolves.
|
|
996
|
-
*/
|
|
997
|
-
export function resolveRustSidecarBin(explicit) {
|
|
998
|
-
if (explicit) {
|
|
999
|
-
return existsSync(explicit) ? explicit : undefined;
|
|
1000
|
-
}
|
|
1001
|
-
const fromEnv = process.env[RUST_SIDECAR_BIN_ENV];
|
|
1002
|
-
if (fromEnv) {
|
|
1003
|
-
return existsSync(fromEnv) ? fromEnv : undefined;
|
|
1004
|
-
}
|
|
1005
|
-
const exe = process.platform === 'win32' ? 'steerable-sidecar.exe' : 'steerable-sidecar';
|
|
1006
|
-
const resourcesPath = process.resourcesPath;
|
|
1007
|
-
if (resourcesPath) {
|
|
1008
|
-
const packaged = join(resourcesPath, 'engine', exe);
|
|
1009
|
-
if (existsSync(packaged))
|
|
1010
|
-
return packaged;
|
|
1011
|
-
}
|
|
1012
|
-
const relatives = [
|
|
1013
|
-
join('..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
|
1014
|
-
join('..', '..', '..', '..', '..', 'sidecar', 'rs', 'target', 'debug', exe),
|
|
1015
|
-
join('..', '..', '..', '..', 'sidecar', 'rs', 'target', 'release', exe),
|
|
1016
|
-
join('..', '..', '..', '..', '..', 'sidecar', 'rs', 'target', 'release', exe),
|
|
1017
|
-
];
|
|
1018
|
-
for (const rel of relatives) {
|
|
1019
|
-
const candidate = join(__dirname, rel);
|
|
1020
|
-
if (existsSync(candidate))
|
|
1021
|
-
return candidate;
|
|
1022
|
-
}
|
|
1023
|
-
return undefined;
|
|
947
|
+
throw new Error('portable Python sidecar runtime not found; prepare python-runtime or set STEERABLE_SIDECAR_PYTHON');
|
|
1024
948
|
}
|
package/dist/sidecar/types.d.ts
CHANGED
|
@@ -21,8 +21,6 @@ export interface SidecarSandboxPosture {
|
|
|
21
21
|
export interface SidecarStartOptions {
|
|
22
22
|
/** Override the python binary; defaults to the bundled portable runtime. */
|
|
23
23
|
pythonExecutable?: string;
|
|
24
|
-
/** Optional Rust sidecar binary; used when STEERABLE_RUST_SIDECAR is on. */
|
|
25
|
-
rustSidecarBin?: string;
|
|
26
24
|
/** Override the entrypoint module; defaults to ``steerable_sidecar``. */
|
|
27
25
|
entryModule?: string;
|
|
28
26
|
/** Extra arguments appended after ``-m <entryModule>``. */
|
package/dist/tool-router.d.ts
CHANGED
|
@@ -76,7 +76,13 @@ export declare const WEB_TOOL_RPC_TIMEOUT_MS = 130000;
|
|
|
76
76
|
* 由 PluginRegistry 背书。宿主只持 schema 并经此 delegate 直调——与 web 工具
|
|
77
77
|
* 不同,这些是普通 RPC 而非 tool.invoke(不经过模型工具分发层)。
|
|
78
78
|
*/
|
|
79
|
-
export type PluginRpcDelegate = (method: 'plugin.list' | 'plugin.enable' | 'plugin.disable' | 'plugin.reload', params?: Record<string, unknown>) => Promise<unknown>;
|
|
79
|
+
export type PluginRpcDelegate = (method: 'plugin.list' | 'plugin.enable' | 'plugin.disable' | 'plugin.reload' | 'plugin.tools.describe', params?: Record<string, unknown>) => Promise<unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* 插件自带工具的执行缝:唯一实现在 sidecar 的插件路由上,宿主只持
|
|
82
|
+
* `plugin.tools.describe` 给出的 schema,调用经 `tool.invoke` 前转(与 web
|
|
83
|
+
* 工具同一模式)。审批已在 sidecar CoreLoop 的 ApprovalExecutor 完成。
|
|
84
|
+
*/
|
|
85
|
+
export type PluginToolDelegate = (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
80
86
|
/**
|
|
81
87
|
* 本机是否提供网络读取工具(`STEERABLE_WEB_TOOLS=0` 关闭)。
|
|
82
88
|
*
|
|
@@ -142,7 +148,18 @@ export declare class ToolRouter {
|
|
|
142
148
|
* plugin_* 工具一个 schema 都不出——模型看不到不可用的工具。
|
|
143
149
|
*/
|
|
144
150
|
private pluginRpc;
|
|
145
|
-
|
|
151
|
+
private pluginToolInvoke;
|
|
152
|
+
/** 已启用插件的工具(名字 → schema),由 refreshPluginTools 从 sidecar 同步。 */
|
|
153
|
+
private pluginToolSchemas;
|
|
154
|
+
setPluginRpc(delegate: PluginRpcDelegate | null, invokeTool?: PluginToolDelegate | null): void;
|
|
155
|
+
private pluginToolsEnabled;
|
|
156
|
+
/**
|
|
157
|
+
* 从 sidecar 拉取已启用插件的工具描述,替换宿主侧的插件工具集。
|
|
158
|
+
* 启动握手后和每次 plugin_enable / plugin_disable / plugin_reload 成功后
|
|
159
|
+
* 调用,保证模型看到的集合与 sidecar 路由上可分发的集合一致。
|
|
160
|
+
* 与内置 / 场景包工具重名的插件工具被跳过并告警——内置工具优先。
|
|
161
|
+
*/
|
|
162
|
+
refreshPluginTools(): Promise<void>;
|
|
146
163
|
listSchemas(): ToolSchema[];
|
|
147
164
|
/**
|
|
148
165
|
* 插件生命周期工具(框架 plugin.* RPC 的模型面)。全在 deferred 层:
|
package/dist/tool-router.js
CHANGED
|
@@ -6,6 +6,7 @@ import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from '
|
|
|
6
6
|
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
7
7
|
import { filterToolsByPolicy, isToolAllowed, } from './local-backend/agent-capability.js';
|
|
8
8
|
import { processViewImage } from './image-attachment.js';
|
|
9
|
+
import { PRESENT_FILES_SCHEMA, PRESENT_FILES_TOOL_NAME, executePresentFiles, } from './present-files.js';
|
|
9
10
|
import { isHostToolCapabilityEnabled } from './host-tools.js';
|
|
10
11
|
import { getResolvedHostTools } from './host-tools-runtime.js';
|
|
11
12
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
@@ -22,6 +23,7 @@ export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
|
22
23
|
export const TOOL_SEARCH_TOOL_NAME = 'tool_search';
|
|
23
24
|
/** 前转 RPC 超时:sidecar 侧单次请求上限默认 30s(可配到 600s),这里给足余量。 */
|
|
24
25
|
export const WEB_TOOL_RPC_TIMEOUT_MS = 130_000;
|
|
26
|
+
const PLUGIN_TOOL_MODES = new Set(['read', 'safe_write', 'destructive']);
|
|
25
27
|
/**
|
|
26
28
|
* 本机是否提供网络读取工具(`STEERABLE_WEB_TOOLS=0` 关闭)。
|
|
27
29
|
*
|
|
@@ -107,8 +109,52 @@ export class ToolRouter {
|
|
|
107
109
|
* plugin_* 工具一个 schema 都不出——模型看不到不可用的工具。
|
|
108
110
|
*/
|
|
109
111
|
pluginRpc = null;
|
|
110
|
-
|
|
112
|
+
pluginToolInvoke = null;
|
|
113
|
+
/** 已启用插件的工具(名字 → schema),由 refreshPluginTools 从 sidecar 同步。 */
|
|
114
|
+
pluginToolSchemas = new Map();
|
|
115
|
+
setPluginRpc(delegate, invokeTool = null) {
|
|
111
116
|
this.pluginRpc = delegate;
|
|
117
|
+
this.pluginToolInvoke = delegate ? invokeTool : null;
|
|
118
|
+
if (!delegate)
|
|
119
|
+
this.pluginToolSchemas = new Map();
|
|
120
|
+
}
|
|
121
|
+
pluginToolsEnabled() {
|
|
122
|
+
return getResolvedHostTools().plugins.capability;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 从 sidecar 拉取已启用插件的工具描述,替换宿主侧的插件工具集。
|
|
126
|
+
* 启动握手后和每次 plugin_enable / plugin_disable / plugin_reload 成功后
|
|
127
|
+
* 调用,保证模型看到的集合与 sidecar 路由上可分发的集合一致。
|
|
128
|
+
* 与内置 / 场景包工具重名的插件工具被跳过并告警——内置工具优先。
|
|
129
|
+
*/
|
|
130
|
+
async refreshPluginTools() {
|
|
131
|
+
if (!this.pluginRpc || !this.pluginToolInvoke) {
|
|
132
|
+
this.pluginToolSchemas = new Map();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const result = (await this.pluginRpc('plugin.tools.describe'));
|
|
136
|
+
this.pluginToolSchemas = new Map();
|
|
137
|
+
const taken = new Set(this.listSchemas().map((s) => s.name));
|
|
138
|
+
const next = new Map();
|
|
139
|
+
for (const tool of result?.tools ?? []) {
|
|
140
|
+
if (!tool || typeof tool.name !== 'string' || !tool.name)
|
|
141
|
+
continue;
|
|
142
|
+
if (taken.has(tool.name)) {
|
|
143
|
+
console.warn(`[tool-router] plugin tool ${tool.name} (${tool.plugin ?? 'unknown plugin'}) shadows a host tool; skipped`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const owner = tool.plugin ? `[plugin:${tool.plugin}] ` : '';
|
|
147
|
+
next.set(tool.name, {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: `${owner}${tool.description || tool.name}`,
|
|
150
|
+
inputSchema: tool.schema && typeof tool.schema === 'object'
|
|
151
|
+
? tool.schema
|
|
152
|
+
: { type: 'object', properties: {} },
|
|
153
|
+
mode: (tool.mode && PLUGIN_TOOL_MODES.has(tool.mode) ? tool.mode : 'external'),
|
|
154
|
+
exposure: 'deferred',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
this.pluginToolSchemas = next;
|
|
112
158
|
}
|
|
113
159
|
listSchemas() {
|
|
114
160
|
return [
|
|
@@ -240,6 +286,7 @@ export class ToolRouter {
|
|
|
240
286
|
required: ['path', 'edits'],
|
|
241
287
|
},
|
|
242
288
|
},
|
|
289
|
+
PRESENT_FILES_SCHEMA,
|
|
243
290
|
{
|
|
244
291
|
name: 'local_open_path',
|
|
245
292
|
description: 'Open local path or URL',
|
|
@@ -374,6 +421,9 @@ export class ToolRouter {
|
|
|
374
421
|
...this.listWebToolSchemas(),
|
|
375
422
|
...this.listRegisteredMcpToolSchemas(),
|
|
376
423
|
...this.listPluginToolSchemas(),
|
|
424
|
+
// 插件自带的工具:与 MCP 动态工具同在 deferred 层,经 tool_search 发现。
|
|
425
|
+
// 名字不带族前缀,按名归族会漏过 plugins 能力开关,这里显式判定。
|
|
426
|
+
...(this.pluginToolsEnabled() ? this.pluginToolSchemas.values() : []),
|
|
377
427
|
// ─── 场景包贡献的工具(0.3a 开槽) ─────────────────
|
|
378
428
|
...[...this.contributedTools.values()].map((c) => ({
|
|
379
429
|
name: c.name,
|
|
@@ -747,6 +797,8 @@ export class ToolRouter {
|
|
|
747
797
|
this.autoInstallSkillOnSuccess(edited, args.path);
|
|
748
798
|
return edited;
|
|
749
799
|
}
|
|
800
|
+
case PRESENT_FILES_TOOL_NAME:
|
|
801
|
+
return await executePresentFiles(args, projectRoot);
|
|
750
802
|
case 'local_open_path':
|
|
751
803
|
return await this.localExecutor.openLocalTarget({
|
|
752
804
|
target: String(args.target || ''),
|
|
@@ -802,7 +854,10 @@ export class ToolRouter {
|
|
|
802
854
|
}
|
|
803
855
|
const method = call.name.replace('_', '.');
|
|
804
856
|
const params = call.name === 'plugin_list' ? undefined : { name: String(args.name || '') };
|
|
805
|
-
|
|
857
|
+
const result = await this.pluginRpc(method, params);
|
|
858
|
+
if (call.name !== 'plugin_list')
|
|
859
|
+
await this.refreshPluginTools();
|
|
860
|
+
return result;
|
|
806
861
|
}
|
|
807
862
|
case 'task_run': {
|
|
808
863
|
const chatId = this.requireTaskContext(context);
|
|
@@ -857,6 +912,12 @@ export class ToolRouter {
|
|
|
857
912
|
if (contributed) {
|
|
858
913
|
return await contributed.handler(args, context ?? {});
|
|
859
914
|
}
|
|
915
|
+
if (this.pluginToolSchemas.has(call.name) && this.pluginToolInvoke) {
|
|
916
|
+
if (!this.pluginToolsEnabled()) {
|
|
917
|
+
throw new Error(`工具 ${call.name} 未在本产品引入`);
|
|
918
|
+
}
|
|
919
|
+
return await this.pluginToolInvoke(call.name, args);
|
|
920
|
+
}
|
|
860
921
|
return { success: false, error: `Unknown tool: ${call.name}` };
|
|
861
922
|
}
|
|
862
923
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.50",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"he": "^1.2.0",
|
|
56
56
|
"node-pty": "^1.1.0",
|
|
57
57
|
"semver": "^7.8.0",
|
|
58
|
-
"@steerable/agent-harness": "0.6.
|
|
59
|
-
"@steerable/pack-sdk": "0.6.
|
|
60
|
-
"@steerable/agent-protocol": "0.6.
|
|
58
|
+
"@steerable/agent-harness": "0.6.50",
|
|
59
|
+
"@steerable/pack-sdk": "0.6.50",
|
|
60
|
+
"@steerable/agent-protocol": "0.6.50"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/better-sqlite3": "^7.6.13",
|