refactor(sandbox): defer native protocol exports (round 3)
This commit is contained in:
@@ -1037,7 +1037,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:25`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-sandbox-policy`
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SandboxProvider } from '@deepseek-ai/dsh-sandbox'
|
||||
import {
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
} from 'node-addon-landlock-run'
|
||||
|
||||
const NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'
|
||||
|
||||
/** Snapshot-only provider that reproduces an older-ABI Landlock launch. */
|
||||
export default class PartialLandlockSandboxProvider extends SandboxProvider {
|
||||
@@ -13,16 +10,16 @@ export default class PartialLandlockSandboxProvider extends SandboxProvider {
|
||||
argv: [
|
||||
'bash',
|
||||
'-c',
|
||||
`printf '%s\\n' '${PARTIAL_ENFORCEMENT_NOTICE}' >&2; exec "$@"`,
|
||||
`printf '%s\\n' '${NOTICE}' >&2; exec "$@"`,
|
||||
'partial-landlock-run',
|
||||
...argv,
|
||||
],
|
||||
enforcement: 'partial',
|
||||
denialSignatures: ['permission denied'],
|
||||
runnerFailureRules: [{
|
||||
allowedExitCodes: [LAUNCHER_FAILURE_EXIT],
|
||||
fatalSignatures: [LAUNCHER_FATAL_PREFIX],
|
||||
informationalLines: [PARTIAL_ENFORCEMENT_NOTICE],
|
||||
allowedExitCodes: [125],
|
||||
fatalSignatures: ['landlock-run: '],
|
||||
informationalLines: [NOTICE],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
"@deepseek-ai/dsh-web": "workspace:*",
|
||||
"@deepseek-ai/dsh-web-fetch-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-workflow-workerthread": "workspace:*",
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:*",
|
||||
"node-addon-landlock-run": "0.0.0-test.0"
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write native/landlock-run/README.md
|
||||
README.md: 483b14df7cb91fc6486f69efc1ddc560ab635ed5
|
||||
README.zh.md: a239889ec68a8d56bb901b17feaac7f7acbf66fa
|
||||
README.md: 19cc18830b90609f648cfb2ce1ee509ad9fe381b
|
||||
README.zh.md: c93c33b843e3afc15b1890ce14edf436e051592c
|
||||
@@ -39,7 +39,7 @@ The public API is intentionally small:
|
||||
- `launcherPath()`: absolute path of this host's launcher (existence deliberately unchecked — the probe is the availability signal).
|
||||
- `probe(launcher?, { timeoutMs? })`: functional enforcement probe — `'full' | 'partial' | 'unusable'`.
|
||||
- `grantArgs({ readOnly?, readWrite? })`: the launcher's grant argv; everything not granted is denied.
|
||||
- `LAUNCHER_BIN`, `LAUNCHER_FAILURE_EXIT` (125), `LAUNCHER_FATAL_PREFIX`, and `PARTIAL_ENFORCEMENT_NOTICE`: contract constants. Every launcher failure uses 125, but a successfully exec'd child may also return 125, so failure attribution requires the fatal prefix too.
|
||||
- `LAUNCHER_BIN` and `LAUNCHER_FAILURE_EXIT` (125): contract constants. A successfully exec'd child may also return 125, so consumers need the fatal diagnostic as well as the status to attribute launcher failure.
|
||||
|
||||
The full binary contract (argv grammar, exit codes, report lines) is pinned in [docs/cli-contract.md](docs/cli-contract.md).
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ if (probe(launcher) !== 'unusable') {
|
||||
- `launcherPath()`:当前宿主启动器的绝对路径(有意不检查是否存在;探测结果才是可用性信号)。
|
||||
- `probe(launcher?, { timeoutMs? })`:功能性强制执行探测,返回 `'full' | 'partial' | 'unusable'`。
|
||||
- `grantArgs({ readOnly?, readWrite? })`:启动器的授权 argv;未授予的一切都被拒绝。
|
||||
- `LAUNCHER_BIN`、`LAUNCHER_FAILURE_EXIT`(125)、`LAUNCHER_FATAL_PREFIX` 和 `PARTIAL_ENFORCEMENT_NOTICE`:契约常量。所有 launcher 失败的退出码都是 125,但成功完成 exec 的子进程也可能返回 125,因此还必须有致命前缀,才能将结果归因为 launcher 失败。
|
||||
- `LAUNCHER_BIN` 和 `LAUNCHER_FAILURE_EXIT`(125):契约常量。成功完成 exec 的子进程也可能返回 125,因此消费者必须同时看到致命诊断和该状态,才能将结果归因为 launcher 失败。
|
||||
|
||||
完整的二进制契约(argv 语法、退出码、报告行)锁定在 [docs/cli-contract.md](docs/cli-contract.md) 中。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLI contract: landlock-run
|
||||
|
||||
This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs` and its protocol constants); changing anything below requires a version bump for the whole package family and a note in the release notes.
|
||||
This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it through the entry package (`launcherPath`/`probe`/`grantArgs`) and the launcher protocol; changing anything below requires a version bump for the whole package family and a note in the release notes.
|
||||
|
||||
## Invocation grammar
|
||||
|
||||
@@ -20,14 +20,14 @@ landlock-run --probe
|
||||
## Exit codes
|
||||
|
||||
- `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run.
|
||||
- After a successful `exec`, every child status is passed through unchanged, including 125. Consumers therefore require both status 125 and a `LAUNCHER_FATAL_PREFIX` line to attribute launcher failure.
|
||||
- After a successful `exec`, every child status is passed through unchanged, including 125. Consumers therefore require both status 125 and a `landlock-run: ` fatal line to attribute launcher failure.
|
||||
- `--probe`: `0` when the kernel enforces (fully or partially), `125` otherwise.
|
||||
|
||||
## Report lines
|
||||
|
||||
- Probe success prints exactly one stdout line: `landlock: fully enforced` or `landlock: partially enforced (older ABI)`. The entry package's `probe()` maps these to `full`/`partial`; a non-zero probe exit maps to `unusable`.
|
||||
- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` (`PARTIAL_ENFORCEMENT_NOTICE`) and proceeds — still confined for everything the kernel supports.
|
||||
- Every fatal error prints one stderr line prefixed `landlock-run: ` (`LAUNCHER_FATAL_PREFIX`) before exiting `125`.
|
||||
- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` and proceeds — still confined for everything the kernel supports.
|
||||
- Every fatal error prints one stderr line prefixed `landlock-run: ` before exiting `125`.
|
||||
|
||||
## Confinement semantics
|
||||
|
||||
|
||||
@@ -21,18 +21,12 @@ import { fileURLToPath } from 'node:url'
|
||||
/** The launcher binary's file name inside each platform package's `bin/`. */
|
||||
export const LAUNCHER_BIN = 'landlock-run'
|
||||
|
||||
/** Prefix on every launcher-owned fatal stderr line. */
|
||||
export const LAUNCHER_FATAL_PREFIX = 'landlock-run: '
|
||||
|
||||
/** Informational stderr line emitted before child execution under partial enforcement. */
|
||||
export const PARTIAL_ENFORCEMENT_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'
|
||||
|
||||
/**
|
||||
* The exit code for every launcher-level failure (usage error, unenforcing
|
||||
* kernel, unopenable grant root, failed exec). After a successful `exec`, the
|
||||
* wrapped command may also return 125, so consumers require a matching
|
||||
* {@link LAUNCHER_FATAL_PREFIX} diagnostic to attribute launcher failure.
|
||||
* Part of the CLI contract.
|
||||
* wrapped command may also return 125, so consumers also require a matching
|
||||
* launcher-owned fatal diagnostic to attribute launcher failure. Part of the
|
||||
* CLI contract.
|
||||
*/
|
||||
export const LAUNCHER_FAILURE_EXIT = 125
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ import path from 'node:path';
|
||||
import {
|
||||
LAUNCHER_BIN,
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
grantArgs,
|
||||
launcherPath,
|
||||
probe,
|
||||
@@ -22,8 +20,6 @@ import {
|
||||
// --- constants are part of the CLI contract ---
|
||||
assert.equal(LAUNCHER_BIN, 'landlock-run');
|
||||
assert.equal(LAUNCHER_FAILURE_EXIT, 125);
|
||||
assert.equal(LAUNCHER_FATAL_PREFIX, 'landlock-run: ');
|
||||
assert.equal(PARTIAL_ENFORCEMENT_NOTICE, 'landlock-run: partial enforcement (older Landlock ABI)');
|
||||
|
||||
// --- grantArgs: flag spelling, ordering, and empty grants ---
|
||||
assert.deepEqual(grantArgs({}), []);
|
||||
|
||||
@@ -19,13 +19,13 @@ import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
grantArgs,
|
||||
launcherPath,
|
||||
probe,
|
||||
} from 'node-addon-landlock-run';
|
||||
|
||||
const FATAL_PREFIX = 'landlock-run: ';
|
||||
const PARTIAL_NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)';
|
||||
const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1';
|
||||
|
||||
if (process.platform !== 'linux') {
|
||||
@@ -45,7 +45,7 @@ const run = (args, options = {}) => spawnSync(launcher, args, { encoding: 'utf8'
|
||||
{
|
||||
const noCommand = run([]);
|
||||
assert.equal(noCommand.status, LAUNCHER_FAILURE_EXIT);
|
||||
assert.ok(noCommand.stderr.startsWith(LAUNCHER_FATAL_PREFIX));
|
||||
assert.ok(noCommand.stderr.startsWith(FATAL_PREFIX));
|
||||
assert.match(noCommand.stderr, /usage error: missing `-- <argv>\.\.\.` command/);
|
||||
|
||||
const unknownFlag = run(['--bogus', '--', 'true']);
|
||||
@@ -78,7 +78,7 @@ if (enforcement === 'unusable') {
|
||||
console.log('launcher.test: SKIP enforcement half — kernel does not enforce Landlock');
|
||||
process.exit(0);
|
||||
}
|
||||
const expectedNotice = enforcement === 'partial' ? `${PARTIAL_ENFORCEMENT_NOTICE}\n` : '';
|
||||
const expectedNotice = enforcement === 'partial' ? `${PARTIAL_NOTICE}\n` : '';
|
||||
{
|
||||
const probeRun = run(['--probe']);
|
||||
assert.equal(probeRun.status, 0);
|
||||
@@ -129,7 +129,7 @@ const expectedNotice = enforcement === 'partial' ? `${PARTIAL_ENFORCEMENT_NOTICE
|
||||
const marker = path.join(os.tmpdir(), `nalr-should-not-exist-${process.pid}`);
|
||||
const badGrant = run(['--ro', '/no/such/grant/root', '--', '/bin/sh', '-c', `echo x > ${marker}`]);
|
||||
assert.equal(badGrant.status, LAUNCHER_FAILURE_EXIT);
|
||||
assert.ok(badGrant.stderr.startsWith(LAUNCHER_FATAL_PREFIX));
|
||||
assert.ok(badGrant.stderr.startsWith(FATAL_PREFIX));
|
||||
assert.match(badGrant.stderr, /cannot open rule path/);
|
||||
assert.ok(!fs.existsSync(marker), 'the command must never run when the launcher fails');
|
||||
}
|
||||
|
||||
@@ -9,18 +9,16 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import {
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
} from 'node-addon-landlock-run'
|
||||
import { LAUNCHER_FAILURE_EXIT } from 'node-addon-landlock-run'
|
||||
import { SANDBOX_UNAVAILABLE } from '@deepseek-ai/dsh-sandbox'
|
||||
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
|
||||
import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
const FATAL = `${LAUNCHER_FATAL_PREFIX}landlock ruleset error: Invalid argument`
|
||||
const NOTICE = 'landlock-run: partial enforcement (older Landlock ABI)'
|
||||
const FATAL_PREFIX = 'landlock-run: '
|
||||
const FATAL = `${FATAL_PREFIX}landlock ruleset error: Invalid argument`
|
||||
|
||||
const contexts: Context[] = []
|
||||
const tempDirs: string[] = []
|
||||
@@ -41,10 +39,10 @@ while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--ro|--rw) shift 2 ;;
|
||||
--) shift; break ;;
|
||||
*) printf '%s\\n' '${LAUNCHER_FATAL_PREFIX}usage error: unexpected fake argument' >&2; exit ${LAUNCHER_FAILURE_EXIT} ;;
|
||||
*) printf '%s\\n' '${FATAL_PREFIX}usage error: unexpected fake argument' >&2; exit ${LAUNCHER_FAILURE_EXIT} ;;
|
||||
esac
|
||||
done
|
||||
printf '%s\\n' '${PARTIAL_ENFORCEMENT_NOTICE}' >&2
|
||||
printf '%s\\n' '${NOTICE}' >&2
|
||||
${fatalBranch}exec "$@"
|
||||
`, { mode: 0o755 })
|
||||
return launcher
|
||||
@@ -113,7 +111,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
const bash = await setup()
|
||||
const result = await bash.run(bash.resolve({ command: `exit ${exitCode}` }))
|
||||
expect(result.exitCode).toBe(exitCode)
|
||||
expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n`)
|
||||
expect(result.stderr.text).toBe(`${NOTICE}\n`)
|
||||
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
|
||||
},
|
||||
)
|
||||
@@ -122,7 +120,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
const bash = await setup()
|
||||
const result = await bash.run(bash.resolve({ command: `exit ${exitCode}` }))
|
||||
expect(result.exitCode).toBe(exitCode)
|
||||
expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n`)
|
||||
expect(result.stderr.text).toBe(`${NOTICE}\n`)
|
||||
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
|
||||
})
|
||||
|
||||
@@ -130,7 +128,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
const bash = await setup(exitCode)
|
||||
const result = await bash.run(bash.resolve({ command: 'true' }))
|
||||
expect(result.exitCode).toBe(exitCode)
|
||||
expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\n${FATAL}\n`)
|
||||
expect(result.stderr.text).toBe(`${NOTICE}\n${FATAL}\n`)
|
||||
expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
|
||||
})
|
||||
|
||||
@@ -140,13 +138,13 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
expect(error).toMatchObject({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE })
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect((error as Error).message).toContain(`Runner failure: ${FATAL}`)
|
||||
expect((error as Error).message).not.toContain(PARTIAL_ENFORCEMENT_NOTICE)
|
||||
expect((error as Error).message).not.toContain(NOTICE)
|
||||
})
|
||||
|
||||
it('classifies a notice plus child Permission denied as a denial, not runner failure', async () => {
|
||||
const bash = await setup()
|
||||
const result = await bash.run(bash.resolve({ command: 'printf "%s\\n" "child: Permission denied" >&2; exit 1' }))
|
||||
expect(result.stderr.text).toBe(`${PARTIAL_ENFORCEMENT_NOTICE}\nchild: Permission denied\n`)
|
||||
expect(result.stderr.text).toBe(`${NOTICE}\nchild: Permission denied\n`)
|
||||
expect(result.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' })
|
||||
})
|
||||
|
||||
@@ -156,7 +154,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
const task = bash.start(bash.resolve({ command }))
|
||||
await task.done
|
||||
expect(task.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'partial' })
|
||||
expect(task.readOutput().delta).toContain(PARTIAL_ENFORCEMENT_NOTICE)
|
||||
expect(task.readOutput().delta).toContain(NOTICE)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -165,7 +163,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
const task = bash.start(bash.resolve({ command: 'printf "%s\\n" "child: Permission denied" >&2; exit 1' }))
|
||||
await task.done
|
||||
expect(task.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'partial' })
|
||||
expect(task.readOutput().delta).toContain(PARTIAL_ENFORCEMENT_NOTICE)
|
||||
expect(task.readOutput().delta).toContain(NOTICE)
|
||||
})
|
||||
|
||||
it('makes a background fatal line outrank denial text after the notice', async () => {
|
||||
@@ -179,7 +177,7 @@ describe('partial Landlock runner-failure classification', () => {
|
||||
runnerFailed: true,
|
||||
})
|
||||
const output = task.readOutput().delta
|
||||
expect(output).toContain(PARTIAL_ENFORCEMENT_NOTICE)
|
||||
expect(output).toContain(NOTICE)
|
||||
expect(output).toContain(FATAL)
|
||||
})
|
||||
})
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import {
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
LAUNCHER_BIN,
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
launcherPath as landlockLauncherPath,
|
||||
probe as defaultProbeLandlock,
|
||||
} from 'node-addon-landlock-run'
|
||||
@@ -157,8 +156,8 @@ const RUNNER_FAILURE_RULES = {
|
||||
bwrap: [{ fatalSignatures: ['bwrap: '] }],
|
||||
landlock: [{
|
||||
allowedExitCodes: [LAUNCHER_FAILURE_EXIT],
|
||||
fatalSignatures: [LAUNCHER_FATAL_PREFIX],
|
||||
informationalLines: [PARTIAL_ENFORCEMENT_NOTICE],
|
||||
fatalSignatures: [`${LAUNCHER_BIN}: `],
|
||||
informationalLines: [`${LAUNCHER_BIN}: partial enforcement (older Landlock ABI)`],
|
||||
}],
|
||||
seatbelt: [{ fatalSignatures: ['sandbox-exec: '] }],
|
||||
} as const satisfies Record<SelectedRunner['runner'], readonly RunnerFailureRule[]>
|
||||
|
||||
@@ -12,11 +12,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import {
|
||||
LAUNCHER_FAILURE_EXIT,
|
||||
LAUNCHER_FATAL_PREFIX,
|
||||
PARTIAL_ENFORCEMENT_NOTICE,
|
||||
} from 'node-addon-landlock-run'
|
||||
import { LAUNCHER_FAILURE_EXIT } from 'node-addon-landlock-run'
|
||||
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import {
|
||||
@@ -179,8 +175,8 @@ describe('the platform chains', () => {
|
||||
denialSignatures: ['permission denied'],
|
||||
runnerFailureRules: [{
|
||||
allowedExitCodes: [LAUNCHER_FAILURE_EXIT],
|
||||
fatalSignatures: [LAUNCHER_FATAL_PREFIX],
|
||||
informationalLines: [PARTIAL_ENFORCEMENT_NOTICE],
|
||||
fatalSignatures: ['landlock-run: '],
|
||||
informationalLines: ['landlock-run: partial enforcement (older Landlock ABI)'],
|
||||
}],
|
||||
})
|
||||
expect(probeLandlock).toHaveBeenCalledWith(launcher)
|
||||
|
||||
Generated
-4
@@ -848,10 +848,6 @@ importers:
|
||||
'@deepseek-ai/dsh-workspace-context':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/context/workspace-context
|
||||
node-addon-landlock-run:
|
||||
specifier: 0.0.0-test.0
|
||||
version: 0.0.0-test.0
|
||||
|
||||
packages/acp/acp:
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk':
|
||||
|
||||
Reference in New Issue
Block a user