Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr-regression-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ jobs:
run: |
npm run sanitizing:file_layout -- --base-ref "${GATE_BASE_REF:-origin/main}"
npm run sanitizing:tauri_edge -- --base-ref "${GATE_BASE_REF:-origin/main}"
npm run sanitizing:disk_access -- --base-ref "${GATE_BASE_REF:-origin/main}"
npm run sanitizing:global_state -- --base-ref "${GATE_BASE_REF:-origin/main}"
npm run sanitizing:process_env -- --base-ref "${GATE_BASE_REF:-origin/main}"

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@
"sanitizing:file_layout": "node scripts/sanitizing_file_layout.mjs",
"sanitizing:tauri_edge": "node scripts/sanitizing_tauri_edge.mjs",
"sanitizing:global_state": "node scripts/sanitizing_global_state.mjs",
"sanitizing:disk_access": "node scripts/sanitizing_disk_access.mjs",
"sanitizing:process_env": "node scripts/sanitizing_process_env.mjs",
"sanitizing:self": "node --test scripts/sanitizing_tests.mjs"
"sanitizing:self": "node --test scripts/sanitizing_tests.mjs",
"sanitizing:containers": "node scripts/sanitizing_containers.mjs"
},
"dependencies": {
"@tauri-apps/api": "^2",
Expand Down
16 changes: 16 additions & 0 deletions scripts/containers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": 1,
"containers": [
{
"id": "RoleExperiment/Attempts",
"name": "attempts.jsonl",
"scope": "src-tauri/src/cli/role_experiment/",
"owner": "src-tauri/src/cli/role_experiment/storage.rs",
"externalLiteralBaseline": 0,
"writerInventory": {
"files": 2,
"sites": 4
}
}
]
}
2 changes: 1 addition & 1 deletion scripts/sanitizing_baseline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function createSnapshot({ baseRef = 'origin/main', cwd, runner = gitRunne
// an entry that matches nothing is only a WARN: these gates compare against the PR base,
// so an entry goes stale once its debt is merged.
export const ALLOWLIST_FILE = 'sanitizing.allowlist.json';
const ALLOWLIST_GATES = ['01', '02', '05', '06', '08', '09', 'cog'];
const ALLOWLIST_GATES = ['01', '02', '05', '06', '07', '08', '09', 'cog'];
export function parseAllowlist(text) {
let document;
try { document = JSON.parse(text); } catch { throw new GateError(`${ALLOWLIST_FILE}: invalid JSON`); }
Expand Down
99 changes: 99 additions & 0 deletions scripts/sanitizing_containers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Initial finite inventory: literals identify only registered, scoped containers.
// Writer calls are syntactic observations, never proof of a container's identity.
import { gateCli, isMain, GateError, codeOrder } from './sanitizing_baseline.mjs';
import { scanRust } from './sanitizing_rust_scan.mjs';
import { qualified } from './sanitizing_gate_support.mjs';

const registryPath = 'scripts/containers.json';
const writerPath = /^(?:(?:std|tokio)::fs::(?:write|File::create|OpenOptions::new)|(?:fs::)?write|(?:File::create|OpenOptions::new))$/;
const rustSource = p => /^src-tauri\/src\/.*\.rs$/.test(p);
const validPath = p => typeof p === 'string' && /^src-tauri\/src\//.test(p) &&

Check warning on line 10 in scripts/sanitizing_containers.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'String#startsWith' method instead.

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3zq_dZV_EiBLVHL&open=AaFvi3zq_dZV_EiBLVHL&pullRequest=3230
!p.includes('..') && !p.includes(String.fromCharCode(92));

Check warning on line 11 in scripts/sanitizing_containers.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String.fromCodePoint()` over `String.fromCharCode()`.

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3zq_dZV_EiBLVHM&open=AaFvi3zq_dZV_EiBLVHM&pullRequest=3230
export function parseRegistry(text) {
let doc;
try { doc = JSON.parse(text); } catch { throw new GateError('containers.json: invalid JSON'); }
if (doc?.version !== 1 || !Array.isArray(doc.containers) || !doc.containers.length)
throw new GateError('containers.json: version 1 and nonempty containers required');
const ids = new Set(), names = new Set();
for (const c of doc.containers) {
if (!c || typeof c.id !== 'string' || !c.id.trim() || ids.has(c.id) ||
typeof c.name !== 'string' || !/^[\w.-]+\.jsonl?$/.test(c.name) ||
!validPath(c.scope) || !c.scope.endsWith('/') ||
!validPath(c.owner) || !c.owner.startsWith(c.scope) || !c.owner.endsWith('.rs') ||
c.externalLiteralBaseline !== 0 ||
!Number.isInteger(c.writerInventory?.files) || c.writerInventory.files < 0 ||
!Number.isInteger(c.writerInventory?.sites) || c.writerInventory.sites < c.writerInventory.files ||
names.has(c.scope + c.name))
throw new GateError('containers.json: invalid/duplicate identity, owner, zero baseline or inventory');
ids.add(c.id); names.add(c.scope + c.name);
}
return doc.containers;
}

export function measureSource(path, text) {
const scan = scanRust(path, text), ts = scan.productionTokens, writers = [];
for (let i = 0; i < ts.length; i++) {
const t = ts[i];
if (t.kind !== 'ident' || ts[i - 1]?.text === '.' ||
ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident') continue;
const r = qualified(scan, ts, i, writerPath);
if (ts[r.end]?.text === '(' && writerPath.test(r.path ?? '') &&
((r.path ?? '').includes('::') || r.binding?.kind === 'import'))
writers.push({ line: t.line, symbol: r.path, ambiguous: !!r.ambiguous });
i = r.end - 1;

Check warning on line 43 in scripts/sanitizing_containers.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this assignment of "i".

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3zq_dZV_EiBLVHN&open=AaFvi3zq_dZV_EiBLVHN&pullRequest=3230
}
return { literals: ts.filter(t => t.kind === 'string').map(t => ({ value: t.value, line: t.line })),
writers };
}

export function runGate(snapshot) {

Check failure on line 49 in scripts/sanitizing_containers.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 35 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3zq_dZV_EiBLVHO&open=AaFvi3zq_dZV_EiBLVHO&pullRequest=3230
const diagnostics = [], metrics = [], inventory = { files: 0, sites: 0 };
try {
const entry = snapshot.headTree.get(registryPath);
if (!entry) throw new GateError('containers.json: registry missing');
const containers = parseRegistry(snapshot.readBlob(entry));
for (const c of containers) if (!snapshot.headTree.has(c.owner))
throw new GateError('containers.json: owner missing: ' + c.owner);
const measurements = new Map();
for (const p of [...snapshot.headTree.keys()].filter(rustSource).sort(codeOrder)) {
const m = measureSource(p, snapshot.readBlob(snapshot.headTree.get(p)));
measurements.set(p, m);
if (m.writers.length) { inventory.files++; inventory.sites += m.writers.length; }
}
const registeredScopes = containers.map(c => c.scope);
const unknown = [...measurements].filter(([p, m]) => m.writers.length &&
!registeredScopes.some(scope => p.startsWith(scope)));
diagnostics.push({ gate: '04', status: 'WARN', rule: 'unclassified_writers',
path: '<unclassified>', line: 1,
reason: 'Syntactic writers outside registered scopes need identity review; no global prohibition.',
files: unknown.length, sites: unknown.reduce((n, [,m]) => n + m.writers.length, 0),
paths: unknown.map(([p]) => p) });
for (const c of containers) {
const external = [], scopedWriters = { files: 0, sites: 0 };
for (const [p, m] of measurements) {
if (!p.startsWith(c.scope)) continue;
if (m.writers.length) { scopedWriters.files++; scopedWriters.sites += m.writers.length; }
if (p !== c.owner) for (const t of m.literals) if (t.value === c.name)
external.push({ path: p, line: t.line });
}
metrics.push({ id: c.id, externalFiles: new Set(external.map(t => t.path)).size,
externalLiterals: external.length, writerInventory: scopedWriters,
recordedWriterInventory: c.writerInventory });
for (const t of external) diagnostics.push({ gate: '04', status: 'FAIL',
rule: 'external_container_literal', ...t, identity: c.id,
reason: 'Registered container name belongs to ' + c.owner,
remedy: 'Use the typed owner API or its name/path projection.' });
if (scopedWriters.files !== c.writerInventory.files || scopedWriters.sites !== c.writerInventory.sites)
diagnostics.push({ gate: '04', status: 'WARN', rule: 'writer_inventory_changed',
identity: c.id, path: c.scope, line: 1, reason: 'Review syntactic scope writers; other containers are not classified yet.' });
}
snapshot.assertStableHead();
const fail = diagnostics.some(d => d.status === 'FAIL');
return { gate: '04', status: fail ? 'FAIL' : 'PASS', exit: fail ? 1 : 0,
baseOid: snapshot.baseOid, headOid: snapshot.headOid, inventory, metrics, diagnostics };
} catch (e) {
return { gate: '04', status: 'ERROR', exit: e.exit ?? 3,
diagnostics: [{ gate: '04', reason: e.message, remedy: 'Repair registry/Git input and rerun.' }] };
}
}
if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate);
55 changes: 55 additions & 0 deletions scripts/sanitizing_disk_access.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Finite lexical calls; receiver methods are an explicitly separate approximation.
import { gateCli, isMain } from './sanitizing_baseline.mjs';
import { runMeasuredGate, metric, hit, warning, qualified } from './sanitizing_gate_support.mjs';

const diskPath = /^(?:(?:std|tokio)::fs::|fs::|(?:File::(?:open|create)|OpenOptions::new)$)/;
const bareOperation = /^(?:read|read_to_string|read_dir|write|metadata|symlink_metadata|canonicalize|copy|rename|create_dir|create_dir_all|remove_file|remove_dir|remove_dir_all|hard_link|soft_link|read_link|set_permissions|try_exists)$/;
const receiverMethod = /^(?:exists|try_exists|metadata|symlink_metadata|canonicalize|is_file|is_dir|read_dir)$/;
const owner = p => /^src-tauri\/src\/(?:filesystem|path_identity|path_utils)(?:\.rs$|\/)/.test(p);
const inUniverse = p => /^src-tauri\/src\/.*\.rs$/.test(p);

function measure(path, scan) {

Check failure on line 11 in scripts/sanitizing_disk_access.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3rS_dZV_EiBLVHJ&open=AaFvi3rS_dZV_EiBLVHJ&pullRequest=3230
const m = metric(), ts = scan.productionTokens;
if (owner(path)) return m;
for (let i = 0; i < ts.length; i++) {
const t = ts[i];
if (t.kind !== 'ident') continue;
if (ts[i - 1]?.text === '.') {
if (receiverMethod.test(t.name) && ts[i + 1]?.text === '(')
hit(m, 'receiver_method_approximation', t.line);
continue;
}
if (ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident') continue;
const r = qualified(scan, ts, i, diskPath);
const value = r.path ?? '';
const ambiguousOperation = r.ambiguous && bareOperation.test(value);
if (ts[r.end]?.text !== '(' || !(diskPath.test(value) || ambiguousOperation)) continue;
// Imports/qualified paths identify std/tokio; bare File is the established
// finite signal unless the resolver proves a local homonymous binding.
warning(m, path, t.line, r);
if (!m.counts.disk_files) hit(m, 'disk_files', t.line);
i = r.end - 1;

Check warning on line 31 in scripts/sanitizing_disk_access.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this assignment of "i".

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi3rS_dZV_EiBLVHK&open=AaFvi3rS_dZV_EiBLVHK&pullRequest=3230
}
return m;
}

export function runGate(snapshot) {
const result = runMeasuredGate(snapshot, {
gate: '07', rules: ['disk_files'], newFileRules: ['disk_files'],
inUniverse, measure, extraChecks() {},
});
if (result.measurements) {
result.approximation = Object.fromEntries(['base', 'head'].map(side => {
const files = result.measurements[side].filter(m => m.counts.receiver_method_approximation);
return [side, { files: files.length, sites: files.reduce((n, m) =>
n + m.counts.receiver_method_approximation, 0), paths: files.map(m => m.path) }];
}));
result.diagnostics.push({ gate: '07', status: 'WARN', rule: 'receiver_method_approximation',
path: '<untyped receivers>', line: 1,
reason: 'Receiver methods are lexical approximations, may be homonyms, and do not enter disk_files totals.',
...result.approximation.head });
}
return result;
}

if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate);
128 changes: 128 additions & 0 deletions scripts/sanitizing_tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -796,3 +796,131 @@
const result=processGate(trees({[p]:code},{[q]:code}));
assert.equal(result.exit,1); assert.ok(result.diagnostics.some(d=>d.status==='WARN'));
});

import { runGate as containersGate, parseRegistry, measureSource } from './sanitizing_containers.mjs';
const attemptsScope = rustRoot + 'cli/role_experiment/';
const attemptsRegistry = () => ({
version: 1,
containers: [{
id: 'RoleExperiment/Attempts', name: 'attempts.jsonl', scope: attemptsScope,
owner: attemptsScope + 'storage.rs', externalLiteralBaseline: 0,
writerInventory: { files: 0, sites: 0 },
}],
});
function containerTree(files = {}, registry = attemptsRegistry()) {
return trees({
'scripts/containers.json': JSON.stringify(registry),
[attemptsScope + 'storage.rs']: 'const NAME: &str = "attempts.jsonl";',
...files,
});
}
test('04 registered literal: external fails, owner and test fixtures pass', () => {
const consumer = attemptsScope + 'resume.rs';
assert.equal(containersGate(containerTree()).exit, 0);
const bad = containersGate(containerTree({ [consumer]: 'fn f() { let p = "attempts.jsonl"; }' }));
assert.equal(bad.exit, 1);
assert.equal(bad.metrics[0].externalLiterals, 1);
assert.equal(containersGate(containerTree({
[attemptsScope + 'tests.rs']: 'fn fixture() { let p = "attempts.jsonl"; }',
[consumer]: '#[cfg(test)] fn fixture() { let p = "attempts.jsonl"; }',
})).exit, 0);
assert.equal(containersGate(containerTree({
[consumer]: '#[cfg(any(test, windows))] fn f() { let p = r#"attempts.jsonl"#; }',
})).exit, 1);
assert.equal(containersGate(containerTree({
[consumer]: '// "attempts.jsonl"\nfn f() { let p = "other.jsonl"; }',
})).exit, 0);
});
test('04 writer inventory recognizes aliases, excludes cfg and warns unknown identities', () => {
const source = 'use std::fs::{write as save, File as F, OpenOptions as O}; ' +
'fn f() { save("unknown.json", ""); F::create("x"); O::new(); } ' +
'#[cfg(test)] fn fixture() { std::fs::write("fixture", ""); }';
assert.equal(measureSource(rustRoot + 'example.rs', source).writers.length, 3);
assert.equal(measureSource(rustRoot + 'example/tests.rs', source).writers.length, 0);
const result = containersGate(containerTree({ [rustRoot + 'example.rs']: source }));
assert.equal(result.exit, 0);
assert.equal(result.inventory.sites, 3);
assert.equal(result.diagnostics.find(d => d.rule === 'unclassified_writers').files, 1);
});
test('04 rejects invalid registries and missing owners rather than passing silently', () => {
for (const mutate of [
r => { r.version = 2; },
r => { r.containers[0].externalLiteralBaseline = 1; },
r => { r.containers[0].owner = '../outside.rs'; },
r => { r.containers[0].writerInventory.sites = -1; },
r => { r.containers.push({ ...r.containers[0] }); },
]) {
const registry = attemptsRegistry(); mutate(registry);
assert.throws(() => parseRegistry(JSON.stringify(registry)));
assert.equal(containersGate(containerTree({}, registry)).status, 'ERROR');
}
assert.throws(() => parseRegistry('{'));
const snapshot = containerTree();
snapshot.headTree.delete(attemptsScope + 'storage.rs');
assert.equal(containersGate(snapshot).status, 'ERROR');
});

import { runGate as diskGate } from './sanitizing_disk_access.mjs';
test('07 calls, aliases, multiline, ambiguity and shadowing', () => {
for (const bad of [
'fn f() { std::fs::read("x"); }',
'use std::fs as disk; fn f() { disk::read("x"); }',
'use std::fs::read as load; fn f() { load("x"); }',
'use std::fs::{File as F, OpenOptions as O}; fn f() { F::open("x"); O::new(); }',
'fn f() { File::create("x"); OpenOptions::new(); }',
'fn f() { std\n::fs\n::read\n("x"); }',
]) control(diskGate, 'disk_files', 'disk.rs', 'fn f() {}', bad);
const p = rustRoot + 'disk.rs';
const ambiguous = diskGate(trees({}, {[p]:
'use std::fs as disk; use other as disk; fn f() { disk::read("x"); }'}));
assert.equal(ambiguous.exit, 1);
assert.ok(ambiguous.diagnostics.some(d => d.status === 'WARN' && d.reason === 'duplicate binding'));
const glob = diskGate(trees({}, {[p]:'use std::fs::*; fn f() { read("x"); }'}));
assert.equal(glob.exit, 1);
assert.ok(glob.diagnostics.some(d => d.reason === 'glob import may bind path'));
for (const clean of [
'fn f() { read("x"); }',
'fn f() { /* std::fs::read("x"); */ let s = "File::open"; }',
'#[cfg(test)] mod tests { fn f() { std::fs::read("x"); } }',
'mod File { fn open() {} } fn f() { File::open(); }',
'use custom::fs; fn f() { fs::read("x"); }',
'use std::fs::read as load; fn f() { let load = || (); load(); }',
]) assert.equal(diskGate(trees({}, {[p]:clean})).exit, 0, clean);
});
test('07 exact owners, tests and separate untyped receiver approximation', () => {
const bad = 'fn f() { std::fs::read("x"); }';
for (const name of ['filesystem.rs', 'filesystem/read.rs', 'path_identity.rs',
'path_identity/read.rs', 'path_utils.rs', 'path_utils/read.rs', 'tests.rs',
'foo/tests/read.rs', 'foo_tests.rs'])
assert.equal(diskGate(trees({}, {[rustRoot + name]:bad})).exit, 0, name);
for (const name of ['filesystem_extra.rs', 'store.rs', 'containers/read.rs'])
assert.equal(diskGate(trees({}, {[rustRoot + name]:bad})).exit, 1, name);
const result = diskGate(trees({}, {[rustRoot + 'model.rs']:
'fn f(record: Record) { record.metadata(); record.exists(); record.canonicalize(); }'}));
assert.equal(result.exit, 0);
assert.equal(result.totals[1].disk_files, 0);
assert.deepEqual(result.approximation.head, {files:1, sites:3, paths:[rustRoot + 'model.rs']});
});
test('07 file counts equal/down/up, no compensated new file or move credit', () => {
const p = rustRoot + 'old.rs', q = rustRoot + 'new.rs';
const bad = 'fn f() { std::fs::read("x"); }';
const base = {[p]:bad};
assert.equal(diskGate(trees(base, {[p]:bad + bad})).exit, 0);
assert.equal(diskGate(trees(base, {[p]:''})).exit, 0);
assert.equal(diskGate(trees(base, {[p]:bad, [q]:bad})).exit, 1);
assert.equal(diskGate(trees(base, {[p]:'', [q]:bad})).exit, 1);
assert.equal(diskGate(trees(base, {[q]:bad})).exit, 1);
});
test('07 immutable real Git base, working tree ignored, drift and blob errors', t => {
const f = fixture(t), p = rustRoot + 'disk.rs';
f.write(p, 'fn f() {}'); const base = f.commit();
f.write(p, 'fn f() { std::fs::read("x"); }'); f.commit();
const snapshot = f.snapshot(base);
f.write(p, 'fn f() {}');
assert.equal(diskGate(snapshot).exit, 1);
f.write('next', 'next'); f.commit();
assert.equal(diskGate(snapshot).exit, 3);
const broken = trees({}, {[p]:'x'});
broken.readBlob = () => { throw Error('missing blob'); };

Check warning on line 924 in scripts/sanitizing_tests.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `new Error()` instead of `Error()`.

See more on https://sonarcloud.io/project/issues?id=mblua_AgentsCommander&issues=AaFvi318_dZV_EiBLVHP&open=AaFvi318_dZV_EiBLVHP&pullRequest=3230
assert.equal(diskGate(broken).status, 'ERROR');
});
3 changes: 3 additions & 0 deletions src-tauri/module-arcs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ agentscommander_lib::app_run::setup -> agentscommander_lib::session::selection
agentscommander_lib::app_run::setup -> agentscommander_lib::testability::ui_automation
agentscommander_lib::app_run::setup -> agentscommander_lib::testability::window_placement
agentscommander_lib::app_run::setup -> agentscommander_lib::update_check
agentscommander_lib::capture::catalog -> agentscommander_lib::filesystem
agentscommander_lib::capture::jev -> agentscommander_lib::capture::catalog
agentscommander_lib::capture::jev -> agentscommander_lib::network
agentscommander_lib::capture::key -> agentscommander_lib::capture::record
Expand Down Expand Up @@ -1166,6 +1167,7 @@ agentscommander_lib::config::activity_log -> agentscommander_lib::session::profi
agentscommander_lib::config::activity_log -> agentscommander_lib::session::session
agentscommander_lib::config::agency_cache -> agentscommander_lib::config
agentscommander_lib::config::agency_cache -> agentscommander_lib::config::role_templates
agentscommander_lib::config::agency_cache -> agentscommander_lib::process
agentscommander_lib::config::agency_cache::git -> agentscommander_lib::config::agent_path
agentscommander_lib::config::agency_cache::git -> agentscommander_lib::config::role_templates
agentscommander_lib::config::agency_cache::git -> agentscommander_lib::pty::credentials
Expand Down Expand Up @@ -2885,6 +2887,7 @@ agentscommander_lib::testability::ui_automation::files -> agentscommander_lib::t
agentscommander_lib::testability::window_info -> agentscommander_lib
agentscommander_lib::testability::window_info::windows_impl -> agentscommander_lib::config::profile
agentscommander_lib::testability::window_info::windows_impl -> agentscommander_lib::testability::window_info
agentscommander_lib::testability::window_placement -> agentscommander_lib::config::environment
agentscommander_lib::update_check -> agentscommander_lib::config
agentscommander_lib::update_check -> agentscommander_lib::config::settings
agentscommander_lib::update_check -> agentscommander_lib::network
Expand Down
Loading
Loading