sync: update from private repo (8ed98ab)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
ef44e1a5d9
commit
d6d8e83867
@ -208,6 +208,10 @@ subtasks:
|
|||||||
# # always sandbox を強制。bwrap 不在なら起動失敗(本番推奨)
|
# # always sandbox を強制。bwrap 不在なら起動失敗(本番推奨)
|
||||||
# # off bwrap を使わない(env スクラブは維持)。デバッグ用、非推奨
|
# # off bwrap を使わない(env スクラブは維持)。デバッグ用、非推奨
|
||||||
# bash_sandbox: auto
|
# bash_sandbox: auto
|
||||||
|
# bash_allow_network: false # true: サンドボックスで host network を許可
|
||||||
|
# # (--unshare-net を外す)。pip/npm install/curl 等が
|
||||||
|
# # 可能になる。⚠ 隔離が弱まり情報漏えい/SSRF のリスク。
|
||||||
|
# # 既定 false (ネット遮断)。sandboxed 時のみ有効。
|
||||||
|
|
||||||
# ─── Search Filter (WebSearch の機密情報漏洩防止) ─────────────
|
# ─── Search Filter (WebSearch の機密情報漏洩防止) ─────────────
|
||||||
# search_filter:
|
# search_filter:
|
||||||
|
|||||||
@ -530,15 +530,23 @@ function buildStorage(out: Record<string, unknown>): StorageConfig {
|
|||||||
// sub-form which binds directly to `tools.*`, so keeping that path
|
// sub-form which binds directly to `tools.*`, so keeping that path
|
||||||
// alive is the cheapest fix for the v2 read path.
|
// alive is the cheapest fix for the v2 read path.
|
||||||
if (storage.taskUploadMaxSizeMb !== undefined) {
|
if (storage.taskUploadMaxSizeMb !== undefined) {
|
||||||
|
// Audit 2026-06-08 fix: a user-authored v2 storage block
|
||||||
|
// (existing.X !== undefined) is authoritative and must override the tools.*
|
||||||
|
// default — matching the worktreeDir precedence above. The prior
|
||||||
|
// `toolsObj.X === undefined` guard was a permanent no-op because defaults
|
||||||
|
// already seeded tools.{taskUploadMaxSizeMb,trashRetentionDays}, so a v2
|
||||||
|
// `storage.*` value was silently ignored (same default-precedence trap as
|
||||||
|
// PR #368→#369). The UI path writes tools.* directly (not storage.*), so
|
||||||
|
// there existing.X is undefined and tools.X is set → no overwrite.
|
||||||
const toolsObj = (out.tools ?? {}) as Record<string, unknown>;
|
const toolsObj = (out.tools ?? {}) as Record<string, unknown>;
|
||||||
if (toolsObj.taskUploadMaxSizeMb === undefined) {
|
if (existing.taskUploadMaxSizeMb !== undefined || toolsObj.taskUploadMaxSizeMb === undefined) {
|
||||||
toolsObj.taskUploadMaxSizeMb = storage.taskUploadMaxSizeMb;
|
toolsObj.taskUploadMaxSizeMb = storage.taskUploadMaxSizeMb;
|
||||||
out.tools = toolsObj;
|
out.tools = toolsObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (storage.trashRetentionDays !== undefined) {
|
if (storage.trashRetentionDays !== undefined) {
|
||||||
const toolsObj = (out.tools ?? {}) as Record<string, unknown>;
|
const toolsObj = (out.tools ?? {}) as Record<string, unknown>;
|
||||||
if (toolsObj.trashRetentionDays === undefined) {
|
if (existing.trashRetentionDays !== undefined || toolsObj.trashRetentionDays === undefined) {
|
||||||
toolsObj.trashRetentionDays = storage.trashRetentionDays;
|
toolsObj.trashRetentionDays = storage.trashRetentionDays;
|
||||||
out.tools = toolsObj;
|
out.tools = toolsObj;
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/config.audit-regression.test.ts
Normal file
80
src/config.audit-regression.test.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { loadConfig } from './config.js';
|
||||||
|
|
||||||
|
// Audit 2026-06-08 (config-consistency, P1×2). Both encode the documented,
|
||||||
|
// expected behavior that the current normalizer/env-override plumbing violates.
|
||||||
|
function withTempConfig(yaml: string, fn: (path: string) => void): void {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'maestro-cfg-audit-'));
|
||||||
|
try {
|
||||||
|
const p = join(dir, 'config.yaml');
|
||||||
|
writeFileSync(p, yaml);
|
||||||
|
fn(p);
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('config audit regression (2026-06-08)', () => {
|
||||||
|
const envSnapshot: Record<string, string | undefined> = {};
|
||||||
|
afterEach(() => {
|
||||||
|
for (const [k, v] of Object.entries(envSnapshot)) {
|
||||||
|
if (v === undefined) delete process.env[k];
|
||||||
|
else process.env[k] = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// P1: defaults seed tools.trashRetentionDays=30 BEFORE the normalizer runs, so
|
||||||
|
// the reverse-backfill (`if (tools.X === undefined)`) is a no-op and a user's
|
||||||
|
// v2 `storage.trash_retention_days` is silently ignored by the actual consumer
|
||||||
|
// (server.ts reads config.tools.trashRetentionDays). Same default-precedence
|
||||||
|
// trap as PR #368→#369.
|
||||||
|
it('v2 storage.trash_retention_days actually takes effect on the consumed value', () => {
|
||||||
|
withTempConfig('config_version: 2\nstorage:\n trash_retention_days: 60\n', (p) => {
|
||||||
|
const config = loadConfig(p);
|
||||||
|
expect(config.tools.trashRetentionDays).toBe(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// P1: OLLAMA_BASE_URL / OLLAMA_MODEL are applied to `provider.*` BEFORE
|
||||||
|
// normalize, but for a v2 config with explicit llm.workers the backfill keeps
|
||||||
|
// the YAML workers and never reads provider — so the documented env override
|
||||||
|
// silently has no effect on the endpoint that is actually called.
|
||||||
|
// The runtime executes config.provider.workers (worker.ts:459), so the env
|
||||||
|
// override must reach THAT array. config.llm.workers is the v2 block that
|
||||||
|
// config-manager persists, so it must stay the explicit YAML value — otherwise
|
||||||
|
// saving any setting while OLLAMA_BASE_URL is set would bake the env value into
|
||||||
|
// the YAML and permanently clobber the user's endpoint (codex P2).
|
||||||
|
it('OLLAMA_BASE_URL overrides the executed (provider) worker but not the persisted llm block', () => {
|
||||||
|
envSnapshot['OLLAMA_BASE_URL'] = process.env['OLLAMA_BASE_URL'];
|
||||||
|
process.env['OLLAMA_BASE_URL'] = 'http://override-host:9999/v1';
|
||||||
|
withTempConfig(
|
||||||
|
'config_version: 2\nllm:\n workers:\n - name: w1\n endpoint: http://yaml-host:11434/v1\n model: m1\n',
|
||||||
|
(p) => {
|
||||||
|
const config = loadConfig(p);
|
||||||
|
expect(config.provider.workers[0]?.endpoint).toBe('http://override-host:9999/v1'); // runtime
|
||||||
|
expect(config.llm.workers[0]?.endpoint).toBe('http://yaml-host:11434/v1'); // persisted, untouched
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The override is scoped to the FIRST/default worker only — a multi-worker
|
||||||
|
// pool (gateway/title/reflection) must NOT have every endpoint clobbered when
|
||||||
|
// a Docker .env sets OLLAMA_BASE_URL (codex P1).
|
||||||
|
it('OLLAMA_BASE_URL leaves additional workers untouched on the runtime array', () => {
|
||||||
|
envSnapshot['OLLAMA_BASE_URL'] = process.env['OLLAMA_BASE_URL'];
|
||||||
|
process.env['OLLAMA_BASE_URL'] = 'http://override-host:9999/v1';
|
||||||
|
withTempConfig(
|
||||||
|
'config_version: 2\nllm:\n workers:\n' +
|
||||||
|
' - name: w1\n endpoint: http://yaml-host:11434/v1\n model: m1\n' +
|
||||||
|
' - name: w2\n endpoint: http://gateway-host:4000/v1\n model: m2\n',
|
||||||
|
(p) => {
|
||||||
|
const config = loadConfig(p);
|
||||||
|
expect(config.provider.workers[0]?.endpoint).toBe('http://override-host:9999/v1');
|
||||||
|
expect(config.provider.workers[1]?.endpoint).toBe('http://gateway-host:4000/v1');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -251,6 +251,17 @@ export interface SafetyConfig {
|
|||||||
* - 'off': 旧来の素 exec(後方互換・デバッグ用、非推奨)
|
* - 'off': 旧来の素 exec(後方互換・デバッグ用、非推奨)
|
||||||
*/
|
*/
|
||||||
bashSandbox?: 'auto' | 'always' | 'off';
|
bashSandbox?: 'auto' | 'always' | 'off';
|
||||||
|
/**
|
||||||
|
* When true, the bwrap sandbox keeps host network access (the `--unshare-net`
|
||||||
|
* isolation is dropped) so sandboxed Bash / python / npm can reach the network
|
||||||
|
* (pip install, npm install, curl, etc.). SECURITY: this weakens isolation —
|
||||||
|
* sandboxed commands can then exfiltrate data and reach the internal network
|
||||||
|
* (SSRF / metadata endpoints). The other --unshare-* isolations remain.
|
||||||
|
* Only meaningful when the sandbox is active (bashSandbox auto/always with bwrap
|
||||||
|
* present); in 'off' / hardened-whitelist fallback the network is already open.
|
||||||
|
* Default: false (network isolated).
|
||||||
|
*/
|
||||||
|
bashAllowNetwork?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillsConfig {
|
export interface SkillsConfig {
|
||||||
@ -791,6 +802,34 @@ export function loadConfig(configPath: string = 'config.yaml'): AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audit 2026-06-08 fix: OLLAMA_BASE_URL / OLLAMA_MODEL must also override the
|
||||||
|
// FIRST (default) worker's endpoint/model post-normalize. The pre-normalize
|
||||||
|
// provider.baseUrl override (above) is lost when a v2 config carries an
|
||||||
|
// explicit `llm.workers` block, because normalizeConfig re-derives the worker
|
||||||
|
// arrays from it and never consults provider.baseUrl, so a fresh v2 install
|
||||||
|
// silently ignored the env override.
|
||||||
|
//
|
||||||
|
// Apply ONLY to provider.workers[0] — the array worker.ts actually executes —
|
||||||
|
// and deliberately NOT to llm.workers. Rationale:
|
||||||
|
// - Scope to workers[0]: config-manager treats only llm.workers[0] as the
|
||||||
|
// env-overridden worker; overwriting every worker would clobber a
|
||||||
|
// multi-worker pool (gateway/title/reflection) the moment a Docker .env
|
||||||
|
// sets OLLAMA_BASE_URL.
|
||||||
|
// - provider only (not llm): config-manager persists the v2 `llm` block and
|
||||||
|
// STRIPS the legacy `provider` block on save (V2_STRIPPED_TOP_LEVEL_KEYS).
|
||||||
|
// Mutating llm.workers here would bake the transient env value into the
|
||||||
|
// saved YAML on the next Settings save, permanently clobbering the user's
|
||||||
|
// explicit endpoint (codex P2). Mutating provider keeps the override
|
||||||
|
// runtime-only — the UI still flags the env override via overriddenByEnv.
|
||||||
|
if (process.env['OLLAMA_BASE_URL']) {
|
||||||
|
const ep = process.env['OLLAMA_BASE_URL'];
|
||||||
|
if (config.provider?.workers?.[0]) config.provider.workers[0].endpoint = ep;
|
||||||
|
}
|
||||||
|
if (process.env['OLLAMA_MODEL']) {
|
||||||
|
const model = process.env['OLLAMA_MODEL'];
|
||||||
|
if (config.provider?.workers?.[0]) config.provider.workers[0].model = model;
|
||||||
|
}
|
||||||
|
|
||||||
const errors = validateConfig(config);
|
const errors = validateConfig(config);
|
||||||
for (const err of errors) {
|
for (const err of errors) {
|
||||||
logger.warn(`Config validation: ${err}`);
|
logger.warn(`Config validation: ${err}`);
|
||||||
|
|||||||
@ -202,3 +202,30 @@ describe('MCP table migrations', () => {
|
|||||||
expect(row.owner_id).toBeNull();
|
expect(row.owner_id).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Audit 2026-06-08 (P1, db schema dual-path): user_gitea_orgs is created ONLY
|
||||||
|
// in Repository.initSchema() — not in schema.sql nor migrate.ts. The task-list
|
||||||
|
// display SELECT (repository.ts:33,39) references it in a correlated subquery,
|
||||||
|
// so any DB built via the migration path alone crashes on the most common
|
||||||
|
// query. This pins the invariant from the rule:
|
||||||
|
// tables/columns must live in BOTH the fresh path and the migration path.
|
||||||
|
describe('audit regression: migration path provides display-join tables (2026-06-08)', () => {
|
||||||
|
let db: Database.Database;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = new Database(':memory:');
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
seedMinimalSchema(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runMigrations creates user_gitea_orgs (referenced by the task-list display SELECT)', () => {
|
||||||
|
runMigrations(db);
|
||||||
|
expect(() =>
|
||||||
|
db.prepare('SELECT MIN(org_name) AS n FROM user_gitea_orgs WHERE org_id = ?').get('x'),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -24,6 +24,22 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
db.exec("ALTER TABLE local_tasks ADD COLUMN owner_id TEXT REFERENCES users(id)");
|
db.exec("ALTER TABLE local_tasks ADD COLUMN owner_id TEXT REFERENCES users(id)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audit 2026-06-08 fix: user_gitea_orgs was created ONLY in
|
||||||
|
// Repository.initSchema(), so a DB built via the migration path alone lacked
|
||||||
|
// it and the task-list display SELECT (correlated subquery on org_name in
|
||||||
|
// repository.ts) crashed with "no such table". Create it here too so the
|
||||||
|
// fresh path (schema.sql) and the migration path stay in sync.
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_gitea_orgs (
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
org_id TEXT NOT NULL,
|
||||||
|
org_name TEXT NOT NULL,
|
||||||
|
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
PRIMARY KEY (user_id, org_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_gitea_orgs_org_id ON user_gitea_orgs(org_id);
|
||||||
|
`);
|
||||||
|
|
||||||
// Add context tracking columns to jobs (if not exists)
|
// Add context tracking columns to jobs (if not exists)
|
||||||
// re-fetch after the owner_id ALTER above to reflect the updated schema
|
// re-fetch after the owner_id ALTER above to reflect the updated schema
|
||||||
const jobsColsAfter = db.prepare("PRAGMA table_info('jobs')").all() as Array<{ name: string }>;
|
const jobsColsAfter = db.prepare("PRAGMA table_info('jobs')").all() as Array<{ name: string }>;
|
||||||
|
|||||||
@ -590,3 +590,15 @@ CREATE TABLE IF NOT EXISTS user_notification_prefs (
|
|||||||
v1_migrated INTEGER NOT NULL DEFAULT 0,
|
v1_migrated INTEGER NOT NULL DEFAULT 0,
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Audit 2026-06-08 fix: user_gitea_orgs (per-user Gitea org cache) was created
|
||||||
|
-- only in Repository.initSchema(); mirrored here (fresh path) and in migrate.ts
|
||||||
|
-- (upgrade path) so the task-list display SELECT never hits "no such table".
|
||||||
|
CREATE TABLE IF NOT EXISTS user_gitea_orgs (
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
org_id TEXT NOT NULL,
|
||||||
|
org_name TEXT NOT NULL,
|
||||||
|
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
PRIMARY KEY (user_id, org_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_gitea_orgs_org_id ON user_gitea_orgs(org_id);
|
||||||
|
|||||||
@ -291,7 +291,7 @@ export async function runPiece(
|
|||||||
spawnSubTask?: (params: { title: string; instruction: string; piece?: string }) => Promise<{ jobId: string; subtaskIndex: number; workspacePath: string }>;
|
spawnSubTask?: (params: { title: string; instruction: string; piece?: string }) => Promise<{ jobId: string; subtaskIndex: number; workspacePath: string }>;
|
||||||
cancelCheck?: () => boolean;
|
cancelCheck?: () => boolean;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
safetyConfig?: { maxIterations?: number; maxRevisits?: number; bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off' };
|
safetyConfig?: { maxIterations?: number; maxRevisits?: number; bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off'; bashAllowNetwork?: boolean };
|
||||||
searchFilter?: SearchFilterConfig;
|
searchFilter?: SearchFilterConfig;
|
||||||
customPiecesDir?: string | string[];
|
customPiecesDir?: string | string[];
|
||||||
contextManager?: ContextManager;
|
contextManager?: ContextManager;
|
||||||
@ -690,7 +690,7 @@ function prepareMovementContext(
|
|||||||
/** Role for the job owner. */
|
/** Role for the job owner. */
|
||||||
notesUserRole?: 'admin' | 'user';
|
notesUserRole?: 'admin' | 'user';
|
||||||
/** Safety config — threaded so prepareMovementContext can propagate bashUnrestricted. */
|
/** Safety config — threaded so prepareMovementContext can propagate bashUnrestricted. */
|
||||||
safetyConfig?: { bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off' };
|
safetyConfig?: { bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off'; bashAllowNetwork?: boolean };
|
||||||
skillCatalog?: import('./skills.js').SkillCatalog;
|
skillCatalog?: import('./skills.js').SkillCatalog;
|
||||||
/** Per-task option: when true, MCP tools are not loaded/dispatched. */
|
/** Per-task option: when true, MCP tools are not loaded/dispatched. */
|
||||||
mcpDisabled?: boolean;
|
mcpDisabled?: boolean;
|
||||||
@ -716,6 +716,7 @@ function prepareMovementContext(
|
|||||||
allowedCommands: movementDef.allowed_commands,
|
allowedCommands: movementDef.allowed_commands,
|
||||||
bashUnrestricted: options?.safetyConfig?.bashUnrestricted,
|
bashUnrestricted: options?.safetyConfig?.bashUnrestricted,
|
||||||
bashSandbox: options?.safetyConfig?.bashSandbox,
|
bashSandbox: options?.safetyConfig?.bashSandbox,
|
||||||
|
bashAllowNetwork: options?.safetyConfig?.bashAllowNetwork,
|
||||||
skillCatalog: options?.skillCatalog,
|
skillCatalog: options?.skillCatalog,
|
||||||
allowedSshConnections: movementDef.allowed_ssh_connections,
|
allowedSshConnections: movementDef.allowed_ssh_connections,
|
||||||
pieceName: piece.name,
|
pieceName: piece.name,
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export interface ToolContext {
|
|||||||
allowedCommands?: string[]; // Bash ツールで許可するコマンド名一覧 (省略時は DEFAULT_ALLOWED_COMMANDS)
|
allowedCommands?: string[]; // Bash ツールで許可するコマンド名一覧 (省略時は DEFAULT_ALLOWED_COMMANDS)
|
||||||
bashUnrestricted?: boolean; // true: skip the command whitelist (bwrap/exec is chosen by bashSandbox, not this)
|
bashUnrestricted?: boolean; // true: skip the command whitelist (bwrap/exec is chosen by bashSandbox, not this)
|
||||||
bashSandbox?: 'auto' | 'always' | 'off'; // サンドボックス機構の選択 (config.safety.bashSandbox 由来)
|
bashSandbox?: 'auto' | 'always' | 'off'; // サンドボックス機構の選択 (config.safety.bashSandbox 由来)
|
||||||
|
bashAllowNetwork?: boolean; // true: bwrap サンドボックスで host network を許可 (--unshare-net を外す。config.safety.bashAllowNetwork 由来)
|
||||||
skillCatalog?: import('../skills.js').SkillCatalog;
|
skillCatalog?: import('../skills.js').SkillCatalog;
|
||||||
toolsConfig?: ToolsConfig;
|
toolsConfig?: ToolsConfig;
|
||||||
searchFilter?: SearchFilterConfig; // AppConfig.searchFilter (トップレベル)
|
searchFilter?: SearchFilterConfig; // AppConfig.searchFilter (トップレベル)
|
||||||
@ -1007,6 +1008,7 @@ async function executeBash(input: Record<string, unknown>, ctx: ToolContext): Pr
|
|||||||
}
|
}
|
||||||
const result: SandboxedBashResult = await executeSandboxedBash(
|
const result: SandboxedBashResult = await executeSandboxedBash(
|
||||||
command, ctx.workspacePath, timeoutSec, BASH_MAX_BUFFER_BYTES, ctx.abortSignal, skillBinds,
|
command, ctx.workspacePath, timeoutSec, BASH_MAX_BUFFER_BYTES, ctx.abortSignal, skillBinds,
|
||||||
|
ctx.bashAllowNetwork === true,
|
||||||
);
|
);
|
||||||
const out = result.isError ? result.output : capOutput(result.output, 'stdout');
|
const out = result.isError ? result.output : capOutput(result.output, 'stdout');
|
||||||
logBashHistory(ctx.workspacePath, command, result.isError, Date.now() - startedAt, {
|
logBashHistory(ctx.workspacePath, command, result.isError, Date.now() - startedAt, {
|
||||||
|
|||||||
@ -245,6 +245,27 @@ describe('buildBwrapArgs sandboxing', () => {
|
|||||||
expect(i).toBeGreaterThan(-1);
|
expect(i).toBeGreaterThan(-1);
|
||||||
expect(args).toContain('HOME');
|
expect(args).toContain('HOME');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('unshares network by default (allowNetwork omitted)', () => {
|
||||||
|
const args = buildBwrapArgs('echo hi', '/work/ws');
|
||||||
|
expect(args).toContain('--unshare-net');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps host network when allowNetwork=true (drops only --unshare-net)', () => {
|
||||||
|
const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, true);
|
||||||
|
expect(args).not.toContain('--unshare-net');
|
||||||
|
// all other isolations remain
|
||||||
|
expect(args).toContain('--unshare-user');
|
||||||
|
expect(args).toContain('--unshare-ipc');
|
||||||
|
expect(args).toContain('--unshare-pid');
|
||||||
|
expect(args).toContain('--unshare-uts');
|
||||||
|
expect(args).toContain('--unshare-cgroup');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unshares network when allowNetwork=false (explicit)', () => {
|
||||||
|
const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, false);
|
||||||
|
expect(args).toContain('--unshare-net');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkBwrapAvailable', () => {
|
describe('checkBwrapAvailable', () => {
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export function buildBwrapArgs(
|
|||||||
workspacePath: string,
|
workspacePath: string,
|
||||||
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
||||||
parentEnv: NodeJS.ProcessEnv = process.env,
|
parentEnv: NodeJS.ProcessEnv = process.env,
|
||||||
|
allowNetwork: boolean = false,
|
||||||
): string[] {
|
): string[] {
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
@ -70,7 +71,13 @@ export function buildBwrapArgs(
|
|||||||
|
|
||||||
args.push('--chdir', workspacePath);
|
args.push('--chdir', workspacePath);
|
||||||
args.push('--die-with-parent');
|
args.push('--die-with-parent');
|
||||||
args.push('--unshare-user', '--unshare-ipc', '--unshare-pid', '--unshare-uts', '--unshare-cgroup', '--unshare-net');
|
// Network isolation is the one --unshare-* that's optional. When
|
||||||
|
// allowNetwork is true (config.safety.bashAllowNetwork) the sandbox keeps host
|
||||||
|
// network so pip/npm/curl work; all other namespaces stay unshared.
|
||||||
|
args.push('--unshare-user', '--unshare-ipc', '--unshare-pid', '--unshare-uts', '--unshare-cgroup');
|
||||||
|
if (!allowNetwork) {
|
||||||
|
args.push('--unshare-net');
|
||||||
|
}
|
||||||
|
|
||||||
args.push('--clearenv');
|
args.push('--clearenv');
|
||||||
const sandboxEnv = buildSandboxEnv(parentEnv, workspacePath);
|
const sandboxEnv = buildSandboxEnv(parentEnv, workspacePath);
|
||||||
@ -124,8 +131,9 @@ export async function executeSandboxedBash(
|
|||||||
maxBuffer: number,
|
maxBuffer: number,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
||||||
|
allowNetwork: boolean = false,
|
||||||
): Promise<SandboxedBashResult> {
|
): Promise<SandboxedBashResult> {
|
||||||
const args = buildBwrapArgs(command, workspacePath, extraReadOnlyBinds);
|
const args = buildBwrapArgs(command, workspacePath, extraReadOnlyBinds, process.env, allowNetwork);
|
||||||
|
|
||||||
if (abortSignal?.aborted) {
|
if (abortSignal?.aborted) {
|
||||||
return { output: 'Cancelled before sandbox bash launch', isError: true };
|
return { output: 'Cancelled before sandbox bash launch', isError: true };
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { Registry } from 'prom-client';
|
import { Registry } from 'prom-client';
|
||||||
import { normalizeToolNameForMetric, BUILTIN_TOOL_NAMES } from './tool-name-allowlist.js';
|
import { normalizeToolNameForMetric, BUILTIN_TOOL_NAMES } from './tool-name-allowlist.js';
|
||||||
import { createWorkerMetrics } from './worker-metrics.js';
|
import { createWorkerMetrics } from './worker-metrics.js';
|
||||||
|
import { TOOL_DEFS as SLIDE_DEFS } from '../engine/tools/slide.js';
|
||||||
|
import { TOOL_DEFS as MSLEARN_DEFS } from '../engine/tools/ms-learn.js';
|
||||||
|
|
||||||
describe('normalizeToolNameForMetric', () => {
|
describe('normalizeToolNameForMetric', () => {
|
||||||
it('passes built-in tool names through verbatim', () => {
|
it('passes built-in tool names through verbatim', () => {
|
||||||
@ -59,3 +61,31 @@ describe('normalizeToolNameForMetric', () => {
|
|||||||
expect(dump).not.toMatch(/tool_name="mcp__/);
|
expect(dump).not.toMatch(/tool_name="mcp__/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Audit 2026-06-08 (P1, metrics): the allowlist carried GHOST names
|
||||||
|
// (CreateSlide, MsLearn{Fetch,Read,Search,Summarize}) that no tool emits, while
|
||||||
|
// the REAL slide (SetTheme/AddSlide/BuildPptx/ResetSlides) and ms-learn
|
||||||
|
// (Search/Fetch/SearchCache/RefreshMicrosoftLearn[Cache]) tools were missing —
|
||||||
|
// so every real call collapsed to 'unknown' in metrics. Pin the allowlist to
|
||||||
|
// the actual TOOL_DEFS so it can't drift again.
|
||||||
|
describe('metrics allowlist ↔ real tool definitions (audit regression)', () => {
|
||||||
|
const realNames = [...Object.keys(SLIDE_DEFS), ...Object.keys(MSLEARN_DEFS)];
|
||||||
|
|
||||||
|
it('every real slide/ms-learn tool normalizes to itself (not "unknown")', () => {
|
||||||
|
for (const name of realNames) {
|
||||||
|
expect(normalizeToolNameForMetric(name)).toBe(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('every real slide/ms-learn tool is present in BUILTIN_TOOL_NAMES', () => {
|
||||||
|
for (const name of realNames) {
|
||||||
|
expect(BUILTIN_TOOL_NAMES.has(name)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries no ghost names that no real tool emits', () => {
|
||||||
|
for (const ghost of ['CreateSlide', 'MsLearnFetch', 'MsLearnRead', 'MsLearnSearch', 'MsLearnSummarize']) {
|
||||||
|
expect(BUILTIN_TOOL_NAMES.has(ghost)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -50,14 +50,14 @@ const BUILTIN_TOOL_NAMES_LIST: ReadonlyArray<string> = [
|
|||||||
// review.ts
|
// review.ts
|
||||||
'BatchReviewTextWithLLM', 'MergeReviewedResults',
|
'BatchReviewTextWithLLM', 'MergeReviewedResults',
|
||||||
// browser.ts
|
// browser.ts
|
||||||
'BrowseWeb',
|
'BrowseWeb', 'BrowseWithSession', 'InteractiveBrowse',
|
||||||
// knowledge.ts
|
// knowledge.ts
|
||||||
'IngestDocument', 'IngestStatus', 'ListDocuments', 'ListNamespaces',
|
'IngestDocument', 'IngestStatus', 'ListDocuments', 'ListNamespaces',
|
||||||
'SearchKnowledge',
|
'SearchKnowledge',
|
||||||
// orchestration.ts
|
// orchestration.ts
|
||||||
'SpawnSubTask',
|
'SpawnSubTask',
|
||||||
// x.ts
|
// x.ts
|
||||||
'XPostDetail', 'XSearch', 'XTimeline', 'XUserPosts',
|
'XFetchCardMedia', 'XPostDetail', 'XSearch', 'XTimeline', 'XUserPosts',
|
||||||
// maps.ts
|
// maps.ts
|
||||||
'GetDirections', 'ReverseGeocode', 'SearchPlaces',
|
'GetDirections', 'ReverseGeocode', 'SearchPlaces',
|
||||||
// youtube.ts
|
// youtube.ts
|
||||||
@ -75,7 +75,8 @@ const BUILTIN_TOOL_NAMES_LIST: ReadonlyArray<string> = [
|
|||||||
// mission.ts
|
// mission.ts
|
||||||
'MissionUpdate',
|
'MissionUpdate',
|
||||||
// user-folder.ts
|
// user-folder.ts
|
||||||
'ListUserAssets', 'ReadUserTemplate', 'RenderUserTemplate', 'RunUserScript',
|
'ListUserAssets', 'ReadUserMemory', 'ReadUserTemplate', 'RenderUserTemplate',
|
||||||
|
'RunUserScript', 'UpdateUserMemory', 'WriteUserScript', 'WriteUserTemplate',
|
||||||
// brainstorm.ts
|
// brainstorm.ts
|
||||||
'Brainstorm',
|
'Brainstorm',
|
||||||
// app-docs.ts
|
// app-docs.ts
|
||||||
@ -88,10 +89,12 @@ const BUILTIN_TOOL_NAMES_LIST: ReadonlyArray<string> = [
|
|||||||
'ReadNote', 'SearchNotes', 'WriteNote',
|
'ReadNote', 'SearchNotes', 'WriteNote',
|
||||||
// dashboard.ts
|
// dashboard.ts
|
||||||
'UpdateDashboardWidget',
|
'UpdateDashboardWidget',
|
||||||
|
// skills.ts
|
||||||
|
'InstallSkill', 'ListSkills', 'ReadSkill',
|
||||||
// ms-learn.ts (Microsoft Learn search)
|
// ms-learn.ts (Microsoft Learn search)
|
||||||
'MsLearnFetch', 'MsLearnRead', 'MsLearnSearch', 'MsLearnSummarize',
|
'FetchMicrosoftLearn', 'RefreshMicrosoftLearnCache', 'SearchMicrosoftLearn', 'SearchMicrosoftLearnCache',
|
||||||
// slide.ts
|
// slide.ts (pptxgenjs)
|
||||||
'CreateSlide',
|
'AddSlide', 'BuildPptx', 'ResetSlides', 'SetTheme',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BUILTIN_TOOL_NAMES: ReadonlySet<string> = new Set<string>([
|
export const BUILTIN_TOOL_NAMES: ReadonlySet<string> = new Set<string>([
|
||||||
|
|||||||
@ -21,6 +21,12 @@ export function AskSubtasksForm({ config, onChange }: SectionFormProps) {
|
|||||||
<FieldInput type="number" value={subtasks.maxDepth ?? ''} onChange={v => onChange('subtasks.maxDepth', v ? Number(v) : undefined)} />
|
<FieldInput type="number" value={subtasks.maxDepth ?? ''} onChange={v => onChange('subtasks.maxDepth', v ? Number(v) : undefined)} />
|
||||||
<HelpText>サブタスクのネスト最大深度</HelpText>
|
<HelpText>サブタスクのネスト最大深度</HelpText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Subtasks: Max Per Parent</FieldLabel>
|
||||||
|
<FieldInput type="number" value={subtasks.maxPerParent ?? ''} onChange={v => onChange('subtasks.maxPerParent', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>1 つの親ジョブが spawn できるサブタスクの最大数。デフォルト: 10</HelpText>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import { ReflectionForm } from './ReflectionForm';
|
|||||||
import { McpForm } from './McpForm';
|
import { McpForm } from './McpForm';
|
||||||
import { SshForm } from './SshForm';
|
import { SshForm } from './SshForm';
|
||||||
import { GatewayServerForm } from './GatewayServerForm';
|
import { GatewayServerForm } from './GatewayServerForm';
|
||||||
|
import { NotesForm } from './NotesForm';
|
||||||
|
import { PushNotificationsForm } from './PushNotificationsForm';
|
||||||
|
|
||||||
import { useAuthState } from '../../App';
|
import { useAuthState } from '../../App';
|
||||||
|
|
||||||
@ -185,6 +187,8 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
|||||||
case 'context': return <ContextForm {...formProps} />;
|
case 'context': return <ContextForm {...formProps} />;
|
||||||
case 'safety': return <SafetyForm {...formProps} />;
|
case 'safety': return <SafetyForm {...formProps} />;
|
||||||
case 'reflection': return <ReflectionForm {...formProps} />;
|
case 'reflection': return <ReflectionForm {...formProps} />;
|
||||||
|
case 'notes': return <NotesForm {...formProps} />;
|
||||||
|
case 'push-notifications': return <PushNotificationsForm {...formProps} />;
|
||||||
|
|
||||||
// ── Tools sub-sections — Step 9 split the legacy grab-bag
|
// ── Tools sub-sections — Step 9 split the legacy grab-bag
|
||||||
// ToolsForm into focused per-category forms. Each binds to the
|
// ToolsForm into focused per-category forms. Each binds to the
|
||||||
|
|||||||
48
ui/src/components/settings/NotesForm.tsx
Normal file
48
ui/src/components/settings/NotesForm.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { HelpText } from './HelpText';
|
||||||
|
import { FieldLabel, FieldInput } from './formUtils';
|
||||||
|
import type { SectionFormProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Knowledge Notes injection budget (`notes.inject.*`). Controls how much
|
||||||
|
* of a subscribed note is injected into the agent's context per job.
|
||||||
|
*/
|
||||||
|
export function NotesForm({ config, onChange }: SectionFormProps) {
|
||||||
|
const inject = config.notes?.inject ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h2 className="text-base font-semibold text-slate-800">Notes Injection</h2>
|
||||||
|
<p className="text-[13px] text-slate-500">
|
||||||
|
購読済みの共有ナレッジノートを、ジョブ実行時にエージェントの context へ注入する際の予算。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Per-Note Max (KB)</FieldLabel>
|
||||||
|
<FieldInput type="number" value={inject.perNoteMaxKb ?? ''}
|
||||||
|
onChange={v => onChange('notes.inject.perNoteMaxKb', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>1 ノートあたり注入する最大サイズ。デフォルト: 8 KB</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Total Max (KB)</FieldLabel>
|
||||||
|
<FieldInput type="number" value={inject.totalMaxKb ?? ''}
|
||||||
|
onChange={v => onChange('notes.inject.totalMaxKb', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>全ノート合算で注入する最大サイズ。デフォルト: 32 KB</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Over-Budget Strategy</FieldLabel>
|
||||||
|
<select
|
||||||
|
value={inject.overBudgetStrategy ?? 'skip_remaining'}
|
||||||
|
onChange={e => onChange('notes.inject.overBudgetStrategy', e.target.value)}
|
||||||
|
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow"
|
||||||
|
>
|
||||||
|
<option value="skip_remaining">skip_remaining(予算超過後のノートは注入しない)</option>
|
||||||
|
<option value="truncate_last">truncate_last(最後のノートを途中で切り詰める)</option>
|
||||||
|
<option value="degrade_to_search">degrade_to_search(注入せず検索ツールに委ねる)</option>
|
||||||
|
</select>
|
||||||
|
<HelpText>合計予算を超えたときの挙動。デフォルト: skip_remaining</HelpText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
ui/src/components/settings/PushNotificationsForm.tsx
Normal file
78
ui/src/components/settings/PushNotificationsForm.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { HelpText } from './HelpText';
|
||||||
|
import { FieldLabel, FieldInput } from './formUtils';
|
||||||
|
import type { SectionFormProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side Web Push (V2) config (`notifications.push.*`): VAPID identity +
|
||||||
|
* delivery tuning. This is the ADMIN counterpart to the per-user browser
|
||||||
|
* subscription UI in NotificationsForm (which only manages this device's
|
||||||
|
* subscription, not whether the server feature is enabled at all).
|
||||||
|
*/
|
||||||
|
export function PushNotificationsForm({ config, onChange }: SectionFormProps) {
|
||||||
|
const push = config.notifications?.push ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h2 className="text-base font-semibold text-slate-800">Web Push (Server)</h2>
|
||||||
|
<p className="text-[13px] text-slate-500">
|
||||||
|
ブラウザ通知 V2(Web Push)のサーバ設定。HTTPS ホスティング必須(iOS は PWA インストールも)。
|
||||||
|
各ユーザーの購読操作は 🔔 Notifications タブで行う。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={push.enabled === true}
|
||||||
|
onChange={e => onChange('notifications.push.enabled', e.target.checked)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
Web Push を有効化
|
||||||
|
</label>
|
||||||
|
<HelpText>マスタースイッチ。デフォルト: 無効(オペレーターが明示的に opt-in)</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>VAPID Subject</FieldLabel>
|
||||||
|
<FieldInput value={push.vapidSubject ?? ''} placeholder="https://example.com/"
|
||||||
|
onChange={v => onChange('notifications.push.vapidSubject', v || undefined)} />
|
||||||
|
<HelpText>RFC 8292 の VAPID subject。汎用 mailto: より運用 URL を推奨</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>VAPID Current Key Path</FieldLabel>
|
||||||
|
<FieldInput value={push.vapidCurrentPath ?? ''} placeholder="./data/secrets/vapid.json"
|
||||||
|
onChange={v => onChange('notifications.push.vapidCurrentPath', v || undefined)} />
|
||||||
|
<HelpText>現行 VAPID 鍵ペアファイルのパス(未生成なら mode 0600 で自動生成)</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>VAPID History Dir</FieldLabel>
|
||||||
|
<FieldInput value={push.vapidHistoryDir ?? ''} placeholder="./data/secrets/vapid-history"
|
||||||
|
onChange={v => onChange('notifications.push.vapidHistoryDir', v || undefined)} />
|
||||||
|
<HelpText>失効した VAPID 鍵を退避するディレクトリ</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Payload Max Bytes</FieldLabel>
|
||||||
|
<FieldInput type="number" value={push.payloadMaxBytes ?? ''}
|
||||||
|
onChange={v => onChange('notifications.push.payloadMaxBytes', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>暗号化前のプッシュペイロード最大バイト数(上限 4096)。デフォルト: 3072</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Queue Concurrency</FieldLabel>
|
||||||
|
<FieldInput type="number" value={push.queueConcurrency ?? ''}
|
||||||
|
onChange={v => onChange('notifications.push.queueConcurrency', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>キューからの同時送信数。デフォルト: 8</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Per-Send Timeout (ms)</FieldLabel>
|
||||||
|
<FieldInput type="number" value={push.perSendTimeoutMs ?? ''}
|
||||||
|
onChange={v => onChange('notifications.push.perSendTimeoutMs', v ? Number(v) : undefined)} />
|
||||||
|
<HelpText>1 送信あたりのタイムアウト(ミリ秒)。デフォルト: 10000</HelpText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -38,6 +38,55 @@ export function SafetyForm({ config, onChange }: SectionFormProps) {
|
|||||||
<HelpText>送信前に prompt がコンテキスト上限の何割を占めたら自動圧縮するか(0.5〜0.95、デフォルト: 0.8)</HelpText>
|
<HelpText>送信前に prompt がコンテキスト上限の何割を占めたら自動圧縮するか(0.5〜0.95、デフォルト: 0.8)</HelpText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Bash サンドボックス</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FieldLabel>Bash Sandbox Mode</FieldLabel>
|
||||||
|
<select
|
||||||
|
value={safety.bashSandbox ?? 'auto'}
|
||||||
|
onChange={e => onChange('safety.bashSandbox', e.target.value)}
|
||||||
|
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow"
|
||||||
|
>
|
||||||
|
<option value="auto">auto(bwrap があれば sandboxed、無ければ hardened-whitelist)</option>
|
||||||
|
<option value="always">always(sandboxed を強制・bwrap 不在なら起動時 fail)</option>
|
||||||
|
<option value="off">off(素 exec・後方互換/非推奨)</option>
|
||||||
|
</select>
|
||||||
|
<HelpText>Bash ツールのサンドボックス機構。デフォルト: auto</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={safety.bashUnrestricted === true}
|
||||||
|
onChange={e => onChange('safety.bashUnrestricted', e.target.checked)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
コマンドのホワイトリストを解除(無制限 Bash)
|
||||||
|
</label>
|
||||||
|
<HelpText>bwrap サンドボックス下で任意コマンドを許可する。ワークスペース(rw)とシステムディレクトリ(ro)のみ bind-mount。bwrap の user-namespace 対応が必要。デフォルト: 無効(セキュリティ上、変更は慎重に)</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={safety.bashAllowNetwork === true}
|
||||||
|
onChange={e => onChange('safety.bashAllowNetwork', e.target.checked)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
サンドボックスでネットワークを許可(bash / python / npm がネット接続可)
|
||||||
|
</label>
|
||||||
|
<HelpText>
|
||||||
|
<span className="text-red-700 dark:text-red-300 font-medium">⚠ セキュリティ警告:</span>{' '}
|
||||||
|
通常はサンドボックス内の bash・python・npm はネット遮断(<code>--unshare-net</code>)されます。
|
||||||
|
有効にすると pip / npm install / curl 等が使えるようになりますが、
|
||||||
|
<strong>サンドボックスの隔離が弱まり、任意のデータ送信(情報漏えい)や内部ネットワーク・メタデータエンドポイントへの到達(SSRF)が可能になります</strong>。
|
||||||
|
信頼できる環境でのみ有効化してください。デフォルト: 無効。
|
||||||
|
(Bash Sandbox Mode が <code>off</code> や bwrap 不在時は元々ネット遮断が無いため、この設定は sandboxed 時のみ効きます)
|
||||||
|
</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">History Summarization</h3>
|
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">History Summarization</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const CONFIG_GROUPS = [
|
|||||||
{ id: 'branding', label: 'Branding' },
|
{ id: 'branding', label: 'Branding' },
|
||||||
{ id: 'paths-storage', label: 'Paths & Storage' },
|
{ id: 'paths-storage', label: 'Paths & Storage' },
|
||||||
{ id: 'execution', label: 'Execution' },
|
{ id: 'execution', label: 'Execution' },
|
||||||
|
{ id: 'push-notifications', label: 'Web Push (Server)' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -56,6 +57,7 @@ const CONFIG_GROUPS = [
|
|||||||
{ id: 'context', label: 'Context' },
|
{ id: 'context', label: 'Context' },
|
||||||
{ id: 'safety', label: 'Safety' },
|
{ id: 'safety', label: 'Safety' },
|
||||||
{ id: 'reflection', label: 'Reflection' },
|
{ id: 'reflection', label: 'Reflection' },
|
||||||
|
{ id: 'notes', label: 'Notes Injection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const SETTINGS_SECTIONS = [
|
|||||||
'branding',
|
'branding',
|
||||||
'paths-storage',
|
'paths-storage',
|
||||||
'execution',
|
'execution',
|
||||||
|
'push-notifications',
|
||||||
// LLM group
|
// LLM group
|
||||||
'llm-workers',
|
'llm-workers',
|
||||||
'gateway-server',
|
'gateway-server',
|
||||||
@ -21,6 +22,7 @@ const SETTINGS_SECTIONS = [
|
|||||||
'context',
|
'context',
|
||||||
'safety',
|
'safety',
|
||||||
'reflection',
|
'reflection',
|
||||||
|
'notes',
|
||||||
// Tools group (sub-sections)
|
// Tools group (sub-sections)
|
||||||
'tools-web',
|
'tools-web',
|
||||||
'tools-browser',
|
'tools-browser',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user