Compare commits
10 Commits
af643c452d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 55ed1bf5a9 | |||
| ad2e6f5f4a | |||
| 2738061b5d | |||
| ffc9b331ea | |||
| 729c8fdd5f | |||
| e56b710344 | |||
| d782308b42 | |||
| 8e8752e519 | |||
| 58f6b108c3 | |||
| cb26a044d3 |
@@ -7,6 +7,7 @@ keys:
|
|||||||
- &admin age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
- &admin age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
||||||
# Generated on the machine it names.
|
# Generated on the machine it names.
|
||||||
- &neogaia age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
- &neogaia age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
||||||
|
- &pikachu age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
# Material belonging to one machine.
|
# Material belonging to one machine.
|
||||||
@@ -18,9 +19,16 @@ creation_rules:
|
|||||||
- *admin
|
- *admin
|
||||||
- *neogaia
|
- *neogaia
|
||||||
|
|
||||||
|
- path_regex: secrets/pikachu\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *pikachu
|
||||||
|
|
||||||
# Material common to every machine, so it is stored once rather than per host.
|
# Material common to every machine, so it is stored once rather than per host.
|
||||||
- path_regex: secrets/shared\.yaml$
|
- path_regex: secrets/shared\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *neogaia
|
- *neogaia
|
||||||
|
- *pikachu
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
|
- Subagent completion delivery is non-blocking through immediate spawn, milestone notifications, retained terminal entries, and `subagent_list` or `subagent_result` retrieval.
|
||||||
|
`subagent_wait` intentionally blocks the parent tool call until its condition or timeout, so do not use it merely to keep background work alive during an interactive workflow.
|
||||||
|
- Nixvim's flake input following the root nixpkgs source does not make its Home Manager module reuse the host's `pkgs` instance.
|
||||||
|
Keep `programs.nixvim.nixpkgs.useGlobalPackages = true` so Nixvim uses the shared package set without warning that its source default was affected.
|
||||||
- This host has no `python` or `python3` command on its ordinary `PATH`.
|
- This host has no `python` or `python3` command on its ordinary `PATH`.
|
||||||
For ad hoc Python, use Nix explicitly, such as `nix shell nixpkgs#python3 -c python3 <script>`.
|
For ad hoc Python, use Nix explicitly, such as `nix shell nixpkgs#python3 -c python3 <script>`.
|
||||||
- ADR bodies are immutable records of decisions as they were made, while frontmatter is mutable.
|
- ADR bodies are immutable records of decisions as they were made, while frontmatter is mutable.
|
||||||
|
|||||||
5
base.nix
5
base.nix
@@ -59,7 +59,10 @@ in
|
|||||||
users.users.${user.name} = {
|
users.users.${user.name} = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
description = user.description;
|
description = user.description;
|
||||||
extraGroups = [ "wheel" ];
|
extraGroups = [
|
||||||
|
"wheel"
|
||||||
|
"storage"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# The shared write group.
|
# The shared write group.
|
||||||
|
|||||||
8
flake.lock
generated
8
flake.lock
generated
@@ -562,11 +562,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785622772,
|
"lastModified": 1785695024,
|
||||||
"narHash": "sha256-0mOr+Jxerr4SU1ksLoSkztYlZ9P/nMB6RlgWygoCHWc=",
|
"narHash": "sha256-DLLk6X5zu3cRT50p18uHVdwjGVtiS0t/661M34q02zU=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"rev": "40b16b87963b085817bfb82c26eef7d0408fa8a5",
|
"rev": "9b2a6bcd583d7d6bf7e5377c3632f601692df209",
|
||||||
"revCount": 52,
|
"revCount": 54,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.alexion.dev/alexion/skills"
|
"url": "https://git.alexion.dev/alexion/skills"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -100,7 +100,11 @@
|
|||||||
|
|
||||||
# `nix flake check` builds each host's toplevel.
|
# `nix flake check` builds each host's toplevel.
|
||||||
checks.x86_64-linux = lib.mapAttrs (
|
checks.x86_64-linux = lib.mapAttrs (
|
||||||
_name: host: host.config.system.build.toplevel
|
name: host:
|
||||||
|
if host.config.warnings == [] then
|
||||||
|
host.config.system.build.toplevel
|
||||||
|
else
|
||||||
|
throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}"
|
||||||
) self.nixosConfigurations;
|
) self.nixosConfigurations;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
config,
|
|
||||||
inputs,
|
inputs,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
@@ -39,9 +38,6 @@
|
|||||||
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
|
|
||||||
# A machine the operator works from, so it admits the workstation keys alone.
|
|
||||||
modules.ssh.authorizedKeys = config.modules.ssh.workstationKeys;
|
|
||||||
|
|
||||||
modules.toolkit.enable = true;
|
modules.toolkit.enable = true;
|
||||||
|
|
||||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
# The walking-skeleton guest, enabled like any module: proves the guest path
|
||||||
@@ -69,6 +65,7 @@
|
|||||||
modules.agents.tools.gitea-axi.enable = true;
|
modules.agents.tools.gitea-axi.enable = true;
|
||||||
modules.agents.pi.enable = true;
|
modules.agents.pi.enable = true;
|
||||||
modules.agents.pi.subagents.maxConcurrent = 8;
|
modules.agents.pi.subagents.maxConcurrent = 8;
|
||||||
|
modules.agents.pi.subagents.recentTerminalTtlMs = 15 * 60 * 1000;
|
||||||
|
|
||||||
modules.desktop.enable = true;
|
modules.desktop.enable = true;
|
||||||
modules.desktop.obsidian.enable = true;
|
modules.desktop.obsidian.enable = true;
|
||||||
|
|||||||
54
hosts/pikachu/default.nix
Normal file
54
hosts/pikachu/default.nix
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
# pikachu — AZW ME Pro server.
|
||||||
|
# Disk layout is in ./disk.nix.
|
||||||
|
# `fileSystems` for the root disk are derived from it.
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./hardware-configuration.nix
|
||||||
|
./disk.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
|
||||||
|
hardware.cpu.intel.updateMicrocode = true;
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
|
||||||
|
zramSwap.enable = true;
|
||||||
|
|
||||||
|
systemd.network = {
|
||||||
|
enable = true;
|
||||||
|
networks."10-uplink" = {
|
||||||
|
matchConfig.MACAddress = "78:55:36:07:af:49";
|
||||||
|
networkConfig.DHCP = "yes";
|
||||||
|
linkConfig.RequiredForOnline = "routable";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
networking.useDHCP = false;
|
||||||
|
|
||||||
|
boot.zfs.forceImportRoot = false;
|
||||||
|
|
||||||
|
modules.zfs = {
|
||||||
|
enable = true;
|
||||||
|
hostId = "2346edbd";
|
||||||
|
pools.pikachu = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
modules.ssh.enable = true;
|
||||||
|
modules.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
|
||||||
|
modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
|
||||||
|
|
||||||
|
modules.git.enable = true;
|
||||||
|
modules.toolkit.enable = true;
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
pciutils
|
||||||
|
smartmontools
|
||||||
|
usbutils
|
||||||
|
];
|
||||||
|
|
||||||
|
time.timeZone = "America/New_York";
|
||||||
|
i18n.defaultLocale = "en_GB.UTF-8";
|
||||||
|
}
|
||||||
32
hosts/pikachu/disk.nix
Normal file
32
hosts/pikachu/disk.nix
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{ ... }:
|
||||||
|
# pikachu's install layout for disko: one NVMe boot disk with an EFI system partition and ext4 root.
|
||||||
|
# The existing 8 TB ZFS mirror is imported by name and is never declared here.
|
||||||
|
{
|
||||||
|
disko.devices.disk.main = {
|
||||||
|
type = "disk";
|
||||||
|
device = "/dev/nvme0n1";
|
||||||
|
content = {
|
||||||
|
type = "gpt";
|
||||||
|
partitions = {
|
||||||
|
ESP = {
|
||||||
|
size = "2G";
|
||||||
|
type = "EF00";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "vfat";
|
||||||
|
mountpoint = "/boot";
|
||||||
|
mountOptions = [ "umask=0077" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
root = {
|
||||||
|
size = "100%";
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
18
hosts/pikachu/hardware-configuration.nix
Normal file
18
hosts/pikachu/hardware-configuration.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{ lib, modulesPath, ... }:
|
||||||
|
# Hardware detected from the Proxmox inventory for this machine.
|
||||||
|
# disko derives the root disk filesystems, none declared here.
|
||||||
|
{
|
||||||
|
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
|
||||||
|
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"ahci"
|
||||||
|
"nvme"
|
||||||
|
"sd_mod"
|
||||||
|
"xhci_pci"
|
||||||
|
];
|
||||||
|
boot.initrd.kernelModules = [ ];
|
||||||
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
}
|
||||||
1
hosts/pikachu/ssh_host_ed25519_key.pub
Normal file
1
hosts/pikachu/ssh_host_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu
|
||||||
1
hosts/pikachu/ssh_host_rsa_key.pub
Normal file
1
hosts/pikachu/ssh_host_rsa_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu
|
||||||
@@ -140,6 +140,22 @@ test("activity exposes ordered transcript events while status and list keep only
|
|||||||
assert.doesNotMatch(JSON.stringify(listed), /private transcript body|secret file contents/u);
|
assert.doesNotMatch(JSON.stringify(listed), /private transcript body|secret file contents/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("status activity history keeps only the 100 most recent summaries", async () => {
|
||||||
|
const runner = new FakeRunner();
|
||||||
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
const accepted = await spawnStarted(supervisor);
|
||||||
|
|
||||||
|
for (let index = 0; index < 150; index += 1) {
|
||||||
|
runner.starts[0].events.running(`tick ${index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = supervisor.status(accepted.id).activityHistory;
|
||||||
|
|
||||||
|
assert.equal(history.length, 100);
|
||||||
|
assert.equal(history[0].summary, "tick 50");
|
||||||
|
assert.equal(history[99].summary, "tick 149");
|
||||||
|
});
|
||||||
|
|
||||||
test("process failure reaches failed with diagnostics", async () => {
|
test("process failure reaches failed with diagnostics", async () => {
|
||||||
const runner = new FakeRunner();
|
const runner = new FakeRunner();
|
||||||
const supervisor = new Supervisor(runner, "/tmp");
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
@@ -303,29 +319,47 @@ test("maxConcurrent preserves queued records", async () => {
|
|||||||
assert.equal(runner.starts.length, 2);
|
assert.equal(runner.starts.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("terminal records stay listed past ttl and remain retrievable until cleared", async () => {
|
test("clearTerminal returns only removed terminal ids", async () => {
|
||||||
|
const runner = new FakeRunner();
|
||||||
|
const supervisor = new Supervisor(runner, "/tmp");
|
||||||
|
const first = await spawnStarted(supervisor, "one");
|
||||||
|
const second = await spawnStarted(supervisor, "two");
|
||||||
|
const running = await spawnStarted(supervisor, "three");
|
||||||
|
|
||||||
|
runner.starts[0].events.completed("one done", "agent_settled");
|
||||||
|
runner.starts[1].events.completed("two done", "agent_settled");
|
||||||
|
|
||||||
|
assert.deepEqual(supervisor.clearTerminal(), [first.id, second.id]);
|
||||||
|
assert.throws(() => supervisor.status(first.id), /unknown subagent id/);
|
||||||
|
assert.throws(() => supervisor.status(second.id), /unknown subagent id/);
|
||||||
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("terminal records expire after ttl while active children remain", async () => {
|
||||||
const runner = new FakeRunner();
|
const runner = new FakeRunner();
|
||||||
const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 });
|
const supervisor = new Supervisor(runner, "/tmp", { recentTerminalTtlMs: 5 });
|
||||||
const completed = await spawnStarted(supervisor, "one");
|
const completed = await spawnStarted(supervisor, "one");
|
||||||
const failed = await spawnStarted(supervisor, "two");
|
const failed = await spawnStarted(supervisor, "two");
|
||||||
|
const running = await spawnStarted(supervisor, "three");
|
||||||
|
|
||||||
runner.starts[0].events.completed("one done", "agent_settled");
|
runner.starts[0].events.completed("one done", "agent_settled");
|
||||||
runner.starts[1].events.failed("two failed");
|
runner.starts[1].events.failed("two failed");
|
||||||
await sleep(10);
|
|
||||||
|
|
||||||
const listedIds = supervisor.list().map((status) => status.id);
|
|
||||||
assert.ok(listedIds.includes(completed.id));
|
|
||||||
assert.ok(listedIds.includes(failed.id));
|
|
||||||
assert.equal(supervisor.result(completed.id).result, "one done");
|
assert.equal(supervisor.result(completed.id).result, "one done");
|
||||||
assert.equal(supervisor.result(failed.id).error, "two failed");
|
assert.equal(supervisor.result(failed.id).error, "two failed");
|
||||||
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
|
|
||||||
(supervisor as Supervisor & { clearTerminal(): void }).clearTerminal();
|
await sleep(20);
|
||||||
|
|
||||||
const afterClearIds = supervisor.list().map((status) => status.id);
|
const listedIds = supervisor.list().map((status) => status.id);
|
||||||
assert.equal(afterClearIds.includes(completed.id), false);
|
assert.equal(listedIds.includes(completed.id), false);
|
||||||
assert.equal(afterClearIds.includes(failed.id), false);
|
assert.equal(listedIds.includes(failed.id), false);
|
||||||
|
assert.equal(listedIds.includes(running.id), true);
|
||||||
assert.throws(() => supervisor.status(completed.id), /unknown subagent id/);
|
assert.throws(() => supervisor.status(completed.id), /unknown subagent id/);
|
||||||
|
assert.throws(() => supervisor.status(failed.id), /unknown subagent id/);
|
||||||
|
assert.throws(() => supervisor.result(completed.id), /unknown subagent id/);
|
||||||
assert.throws(() => supervisor.result(failed.id), /unknown subagent id/);
|
assert.throws(() => supervisor.result(failed.id), /unknown subagent id/);
|
||||||
|
assert.equal(supervisor.status(running.id).state, "running");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("zero recent terminal ttl does not hide terminal statuses", async () => {
|
test("zero recent terminal ttl does not hide terminal statuses", async () => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface RunningChild {
|
|||||||
handle?: ChildHandle;
|
handle?: ChildHandle;
|
||||||
startTimer?: ReturnType<typeof setTimeout>;
|
startTimer?: ReturnType<typeof setTimeout>;
|
||||||
runTimer?: ReturnType<typeof setTimeout>;
|
runTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
expiryTimer?: ReturnType<typeof setTimeout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SupervisorOptions {
|
interface SupervisorOptions {
|
||||||
@@ -44,6 +45,8 @@ const DEFAULT_TIMEOUTS = {
|
|||||||
runMs: 0,
|
runMs: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_ACTIVITY_HISTORY = 100;
|
||||||
|
|
||||||
export class Supervisor {
|
export class Supervisor {
|
||||||
private nextChild = 0;
|
private nextChild = 0;
|
||||||
private readonly children = new Map<string, RunningChild>();
|
private readonly children = new Map<string, RunningChild>();
|
||||||
@@ -90,14 +93,15 @@ export class Supervisor {
|
|||||||
return cloneResult(this.require(id).record);
|
return cloneResult(this.require(id).record);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTerminal(ids?: string[]): SubagentStatus[] {
|
clearTerminal(ids?: string[]): string[] {
|
||||||
const selectedIds = ids ? [...new Set(ids.map((id) => id.trim()).filter(Boolean))] : undefined;
|
const selectedIds = ids ? [...new Set(ids.map((id) => id.trim()).filter(Boolean))] : undefined;
|
||||||
if (selectedIds) for (const id of selectedIds) this.require(id);
|
if (selectedIds) for (const id of selectedIds) this.require(id);
|
||||||
const cleared: SubagentStatus[] = [];
|
const cleared: string[] = [];
|
||||||
for (const [id, child] of this.children) {
|
for (const [id, child] of this.children) {
|
||||||
if (selectedIds && !selectedIds.includes(id)) continue;
|
if (selectedIds && !selectedIds.includes(id)) continue;
|
||||||
if (!isTerminal(child.record.status.state)) continue;
|
if (!isTerminal(child.record.status.state)) continue;
|
||||||
cleared.push(cloneStatus(child.record.status));
|
this.clearTimer(child, "expiryTimer");
|
||||||
|
cleared.push(id);
|
||||||
this.children.delete(id);
|
this.children.delete(id);
|
||||||
}
|
}
|
||||||
if (cleared.length > 0) this.emitChange();
|
if (cleared.length > 0) this.emitChange();
|
||||||
@@ -157,6 +161,7 @@ export class Supervisor {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
for (const child of this.children.values()) this.clearTimer(child, "expiryTimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private createChild(request: SpawnRequest): SpawnAccepted {
|
private createChild(request: SpawnRequest): SpawnAccepted {
|
||||||
@@ -248,7 +253,10 @@ export class Supervisor {
|
|||||||
this.recordActivity(record, "completed", now);
|
this.recordActivity(record, "completed", now);
|
||||||
record.status.stopReason = stopReason;
|
record.status.stopReason = stopReason;
|
||||||
record.status.resultAvailable = true;
|
record.status.resultAvailable = true;
|
||||||
if (child) this.emitMilestone(child, "completed");
|
if (child) {
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
|
this.emitMilestone(child, "completed");
|
||||||
|
}
|
||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
},
|
},
|
||||||
failed: (error) => this.fail(record, error),
|
failed: (error) => this.fail(record, error),
|
||||||
@@ -267,7 +275,10 @@ export class Supervisor {
|
|||||||
this.recordActivity(record, "failed", now);
|
this.recordActivity(record, "failed", now);
|
||||||
record.status.error = error;
|
record.status.error = error;
|
||||||
record.status.stopReason = "failed";
|
record.status.stopReason = "failed";
|
||||||
if (child) this.emitMilestone(child, "failed");
|
if (child) {
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
|
this.emitMilestone(child, "failed");
|
||||||
|
}
|
||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +292,7 @@ export class Supervisor {
|
|||||||
child.record.status.lastEventAt = now;
|
child.record.status.lastEventAt = now;
|
||||||
this.recordActivity(child.record, state, now);
|
this.recordActivity(child.record, state, now);
|
||||||
child.record.status.stopReason = reason;
|
child.record.status.stopReason = reason;
|
||||||
|
this.armTerminalExpiry(child);
|
||||||
this.emitMilestone(child, state);
|
this.emitMilestone(child, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,12 +319,26 @@ export class Supervisor {
|
|||||||
this.pumpQueue();
|
this.pumpQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private armTerminalExpiry(child: RunningChild) {
|
||||||
|
const ttl = this.options.recentTerminalTtlMs;
|
||||||
|
if (ttl === undefined || ttl <= 0) return;
|
||||||
|
this.clearTimer(child, "expiryTimer");
|
||||||
|
child.expiryTimer = setTimeout(() => {
|
||||||
|
child.expiryTimer = undefined;
|
||||||
|
const id = child.record.status.id;
|
||||||
|
if (this.children.get(id) !== child || !isTerminal(child.record.status.state)) return;
|
||||||
|
this.children.delete(id);
|
||||||
|
this.emitChange();
|
||||||
|
}, ttl);
|
||||||
|
child.expiryTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
private clearTimers(child: RunningChild) {
|
private clearTimers(child: RunningChild) {
|
||||||
this.clearTimer(child, "startTimer");
|
this.clearTimer(child, "startTimer");
|
||||||
this.clearTimer(child, "runTimer");
|
this.clearTimer(child, "runTimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearTimer(child: RunningChild, key: "startTimer" | "runTimer") {
|
private clearTimer(child: RunningChild, key: "startTimer" | "runTimer" | "expiryTimer") {
|
||||||
const timer = child[key];
|
const timer = child[key];
|
||||||
if (!timer) return;
|
if (!timer) return;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@@ -344,6 +370,9 @@ export class Supervisor {
|
|||||||
const summary = summarizeActivity(activity);
|
const summary = summarizeActivity(activity);
|
||||||
record.status.currentActivity = summary;
|
record.status.currentActivity = summary;
|
||||||
record.status.activityHistory.push(summary);
|
record.status.activityHistory.push(summary);
|
||||||
|
if (record.status.activityHistory.length > MAX_ACTIVITY_HISTORY) {
|
||||||
|
record.status.activityHistory.splice(0, record.status.activityHistory.length - MAX_ACTIVITY_HISTORY);
|
||||||
|
}
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/codin
|
|||||||
diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
|
diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
|
||||||
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:41:36.963495957 -0400
|
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:41:36.963495957 -0400
|
||||||
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:43:04.876341236 -0400
|
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:43:04.876341236 -0400
|
||||||
@@ -210,6 +210,45 @@
|
@@ -210,6 +210,47 @@
|
||||||
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,9 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
|
|||||||
+ private renderGroup(children: Component[], width: number): string[] {
|
+ private renderGroup(children: Component[], width: number): string[] {
|
||||||
+ const lines: string[] = [];
|
+ const lines: string[] = [];
|
||||||
+ for (const child of children) {
|
+ for (const child of children) {
|
||||||
+ lines.push(...child.render(width));
|
+ for (const line of child.render(width)) {
|
||||||
|
+ lines.push(line);
|
||||||
|
+ }
|
||||||
+ }
|
+ }
|
||||||
+ return lines;
|
+ return lines;
|
||||||
+ }
|
+ }
|
||||||
@@ -78,7 +80,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
|
|||||||
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
||||||
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings.";
|
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings.";
|
||||||
|
|
||||||
@@ -335,6 +374,7 @@
|
@@ -335,6 +376,7 @@
|
||||||
private fdPath: string | undefined;
|
private fdPath: string | undefined;
|
||||||
private editorContainer: Container;
|
private editorContainer: Container;
|
||||||
private footer: FooterComponent;
|
private footer: FooterComponent;
|
||||||
@@ -86,7 +88,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
|
|||||||
private footerDataProvider: FooterDataProvider;
|
private footerDataProvider: FooterDataProvider;
|
||||||
// Stored so the same manager can be injected into custom editors, selectors, and extension UI.
|
// Stored so the same manager can be injected into custom editors, selectors, and extension UI.
|
||||||
private keybindings: KeybindingsManager;
|
private keybindings: KeybindingsManager;
|
||||||
@@ -477,7 +517,9 @@
|
@@ -477,7 +519,9 @@
|
||||||
this.editorContainer = new Container();
|
this.editorContainer = new Container();
|
||||||
this.editorContainer.addChild(this.editor as Component);
|
this.editorContainer.addChild(this.editor as Component);
|
||||||
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
||||||
@@ -96,7 +98,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
|
|||||||
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||||
|
|
||||||
// Load hide thinking block setting
|
// Load hide thinking block setting
|
||||||
@@ -704,19 +746,25 @@
|
@@ -704,19 +748,25 @@
|
||||||
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +136,7 @@ diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/p
|
|||||||
this.ui.setFocus(this.editor);
|
this.ui.setFocus(this.editor);
|
||||||
|
|
||||||
this.setupKeyHandlers();
|
this.setupKeyHandlers();
|
||||||
@@ -2033,25 +2081,25 @@
|
@@ -2033,25 +2083,25 @@
|
||||||
| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
|
| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
|
||||||
| undefined,
|
| undefined,
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ in
|
|||||||
|
|
||||||
image = wallpaper;
|
image = wallpaper;
|
||||||
|
|
||||||
|
# Regreet is not enabled, so its styling target stays off.
|
||||||
|
targets.regreet.enable = false;
|
||||||
|
|
||||||
cursor = {
|
cursor = {
|
||||||
package = pkgs.bibata-cursors;
|
package = pkgs.bibata-cursors;
|
||||||
# Solid white with a dark outline, so it stays easy to spot against the
|
# Solid white with a dark outline, so it stays easy to spot against the
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
shellAliases = {
|
shellAliases = {
|
||||||
ls = "eza -al --color=always --group-directories-first --icons=always";
|
ls = "eza -alg --color=always --group-directories-first --icons=always";
|
||||||
la = "eza -a --color=always --group-directories-first --icons=always";
|
la = "eza -a --color=always --group-directories-first --icons=always";
|
||||||
ll = "eza -l --color=always --group-directories-first --icons=always";
|
ll = "eza -lg --color=always --group-directories-first --icons=always";
|
||||||
lt = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
lt = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||||
"l." = "eza -a | grep -e '^\\.'";
|
"l." = "eza -a | grep -e '^\\.'";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ in
|
|||||||
home-manager.users.${user} = hm: {
|
home-manager.users.${user} = hm: {
|
||||||
programs.nixvim = {
|
programs.nixvim = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
nixpkgs.useGlobalPackages = true;
|
||||||
|
|
||||||
extraPackages = with pkgs; [
|
extraPackages = with pkgs; [
|
||||||
git # neogit and gitsigns shell out to git
|
git # neogit and gitsigns shell out to git
|
||||||
|
|||||||
352
modules/ssh.nix
352
modules/ssh.nix
@@ -8,119 +8,298 @@ let
|
|||||||
cfg = config.modules.ssh;
|
cfg = config.modules.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
|
inherit (lib)
|
||||||
|
concatLists
|
||||||
|
concatStringsSep
|
||||||
|
elem
|
||||||
|
filter
|
||||||
|
genAttrs
|
||||||
|
hasAttr
|
||||||
|
imap0
|
||||||
|
listToAttrs
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
types
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
|
targetType = types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
hostName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
description = ''
|
||||||
|
The network address OpenSSH connects to for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = config.user.name;
|
||||||
|
description = ''
|
||||||
|
The remote login name OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 22;
|
||||||
|
description = ''
|
||||||
|
The TCP port OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
aliases = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ name ];
|
||||||
|
description = ''
|
||||||
|
Host patterns written into the generated OpenSSH client block.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
clientKey = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The public key this target offers when it connects outward.
|
||||||
|
Other machines admit this key according to the host groups below.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
The public keys this target presents when it accepts inbound SSH.
|
||||||
|
These keys generate system-wide known-host entries.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
||||||
|
|
||||||
|
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
||||||
|
|
||||||
|
workstationNames = targetNamesIn cfg.hosts.workstations;
|
||||||
|
serverNames = targetNamesIn cfg.hosts.servers;
|
||||||
|
|
||||||
|
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
||||||
|
|
||||||
|
currentHost = config.networking.hostName;
|
||||||
|
isServer = elem currentHost cfg.hosts.servers;
|
||||||
|
isWorkstation = elem currentHost cfg.hosts.workstations;
|
||||||
|
|
||||||
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
|
clientKeysFor workstationNames
|
||||||
|
++ optional isServer (clientKeysFor serverNames)
|
||||||
|
);
|
||||||
|
|
||||||
|
outboundTargetNames =
|
||||||
|
let
|
||||||
|
groupTargets =
|
||||||
|
if isServer then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers
|
||||||
|
else if isWorkstation then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
||||||
|
else
|
||||||
|
[ "gitea" ];
|
||||||
|
in
|
||||||
|
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
||||||
|
|
||||||
|
sshSettingsFor = name:
|
||||||
|
let
|
||||||
|
target = cfg.targets.${name};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
header = "Host ${concatStringsSep " " target.aliases}";
|
||||||
|
HostName = target.hostName;
|
||||||
|
User = target.user;
|
||||||
|
}
|
||||||
|
// optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
|
knownHostNamesFor = target:
|
||||||
|
let
|
||||||
|
names = unique (target.aliases ++ [ target.hostName ]);
|
||||||
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
|
in
|
||||||
|
map withPort names;
|
||||||
|
|
||||||
|
knownHosts = listToAttrs (
|
||||||
|
concatLists (
|
||||||
|
mapAttrsToList (
|
||||||
|
targetName: target:
|
||||||
|
imap0 (i: key:
|
||||||
|
nameValuePair "${targetName}-${toString i}" {
|
||||||
|
hostNames = knownHostNamesFor target;
|
||||||
|
publicKey = keyWithoutComment key;
|
||||||
|
}
|
||||||
|
) target.hostKeys
|
||||||
|
) cfg.targets
|
||||||
|
)
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.ssh = {
|
options.modules.ssh = {
|
||||||
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
||||||
|
|
||||||
workstationKeys = lib.mkOption {
|
hosts = {
|
||||||
type = lib.types.listOf lib.types.str;
|
servers = mkOption {
|
||||||
default = [
|
type = types.listOf types.str;
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia"
|
default = [ "pikachu" ];
|
||||||
];
|
|
||||||
description = ''
|
description = ''
|
||||||
Client public keys of the machines the operator works from.
|
Hosts that serve durable services.
|
||||||
|
They admit workstation keys and server keys, and they receive aliases for other servers and the forge.
|
||||||
Every machine admits these, so any of them reaches the whole fleet.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
serverKeys = lib.mkOption {
|
workstations = mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = types.listOf types.str;
|
||||||
|
default = [ "neogaia" ];
|
||||||
|
description = ''
|
||||||
|
Hosts the operator works from.
|
||||||
|
Their keys are admitted by every host, and they receive aliases for the whole fleet and the forge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
targets = mkOption {
|
||||||
|
type = types.attrsOf targetType;
|
||||||
|
default = {
|
||||||
|
gitea = {
|
||||||
|
hostName = "git.alexion.dev";
|
||||||
|
port = 2022;
|
||||||
|
user = "gitea";
|
||||||
|
aliases = [
|
||||||
|
"gitea"
|
||||||
|
"git.alexion.dev"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
neogaia = {
|
||||||
|
hostName = "10.23.50.146";
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo2wWUKxyAS4J5TqbWf8glDhJvS5XmdRqFhMeJwG3pOB+4AccZ1T8LU7ZN+RjtRi3j2qXBJvIHuzhtQNtmT59TxocvfobYiqOgJpvVO5K6yD8ZoUJs6ziDkIduI9w9mdRIESoi+dBbVu8n24r61cKDVh+jWX+yjzkOcWcOzqDyQhhkjqblZ1WMAdujEMuEPvif1i2LCxStUaZqRGcx09m/ME2fYcaJrpuxxxvX2+CPJNicoo6Rx9i7ZjAoNuvH+jui4KT62DzlQtQtCl2CFUOM0gCPSa+MbNQ9elfHPvGzEcwOIMo2cuy9KURUkQu+sAgaG8S1PEniDDTecskHtuRdmPZawnQGpIhzo919Q6wUgjT8scK4mmSXRWmGmkMt0GNA2tfj5tDks6r5Q8XsYqtWs4rsOEvfmxVSdM771w+fqDBAil99Jsh0ksPK9+Bwgg8cMDzLLFDn8JA5y2G1HocMMom+u5DYKwPXEKnCILkasB8y24+O3PhSu1EuWw277w6EUEXvU03rCf0Ak/ULjxp9a00EGlloEwSmFI7Aub9XHDr87IdbGInEn+PMqyBYADiN+3h6nE2JO+nMa6i/CHdebmT+T7YJvuTKHD9sjFmQsYaghlq03DZrhHcm4hgUvE1dqGojHrhk/WgA3EWTWtK/+BP0Vy2jXaaz+qAx+EGnhQ== neogaia"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
pikachu = {
|
||||||
|
hostName = "10.23.10.102";
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
SSH targets known to the fleet.
|
||||||
|
The inventory holds connection details plus public keys used for authorization and host verification.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraAuthorizedKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Client public keys of the machines that serve.
|
Additional client public keys admitted by this host.
|
||||||
|
|
||||||
Only other servers admit these, so one that is compromised reaches no
|
|
||||||
machine the operator works from.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
authorizedKeys = lib.mkOption {
|
authorizedKeys = mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = types.nullOr (types.listOf types.str);
|
||||||
default = cfg.workstationKeys;
|
|
||||||
defaultText = lib.literalExpression "config.modules.ssh.workstationKeys";
|
|
||||||
description = ''
|
|
||||||
Client public keys this machine admits for the primary user, drawn from
|
|
||||||
the lists above.
|
|
||||||
|
|
||||||
A machine the operator works from takes the workstation keys. One that
|
|
||||||
serves takes both, so servers reach each other. The default admits the
|
|
||||||
workstation keys, since a machine admitting none is unreachable.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostKeys.restore = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Restore the host keys from secrets rather than letting the daemon
|
|
||||||
generate its own.
|
|
||||||
|
|
||||||
A machine with its own identity keeps its fingerprint across a reimage
|
|
||||||
by restoring committed keys. A guest carries no host identity, so it
|
|
||||||
turns this off and presents a self-generated key instead.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostKeys.sopsFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this host's SSH host private keys, one entry per
|
Complete override for client public keys admitted by this host.
|
||||||
key type, named `ssh-host-<type>-key`. Required when `restore` is on.
|
Leave null to derive access from `modules.ssh.hosts` and `modules.ssh.targets`.
|
||||||
|
|
||||||
These are the keys the daemon presents to identify itself to connecting
|
|
||||||
clients, not keys used to authenticate anyone to a remote server.
|
|
||||||
Restoring them from secrets rather than generating them keeps the host's
|
|
||||||
fingerprint across a reimage, so every client's `known_hosts` entry
|
|
||||||
stays valid.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.types = lib.mkOption {
|
extraSettings = mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = types.attrsOf types.anything;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Additional OpenSSH client settings merged into the generated Home Manager configuration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.restore = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Restore the host keys from secrets rather than letting the daemon generate its own.
|
||||||
|
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
||||||
|
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.sopsFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
||||||
|
Required when `restore` is on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.types = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
default = [
|
default = [
|
||||||
"ed25519"
|
"ed25519"
|
||||||
"rsa"
|
"rsa"
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Key types to restore, naming both the entries read from the encrypted
|
Key types to restore, naming both the entries read from the encrypted file and the algorithms the daemon offers.
|
||||||
file and the algorithms the daemon offers. Dropping a type a client has
|
|
||||||
already pinned makes the host unrecognisable to it.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
userKey.sopsFile = lib.mkOption {
|
userKey.sopsFile = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this machine's SSH client private key, under the
|
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
||||||
entry `ssh-user-ed25519-key`. Left unset on a machine that authenticates
|
Left unset on a machine that authenticates to no remote server, such as a guest.
|
||||||
to no remote server, such as a guest.
|
|
||||||
|
|
||||||
This is the key the primary user offers to authenticate to a remote
|
|
||||||
server, not a key the daemon presents to identify this machine.
|
|
||||||
It belongs to this machine alone, so withdrawing its access does not
|
|
||||||
re-key any other.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = mkIf cfg.enable (
|
||||||
lib.mkMerge [
|
mkMerge [
|
||||||
{
|
{
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
|
|
||||||
# The primary user is the only account reachable over SSH.
|
users.users.${user}.openssh.authorizedKeys.keys =
|
||||||
users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys;
|
if cfg.authorizedKeys != null then
|
||||||
|
cfg.authorizedKeys
|
||||||
|
else
|
||||||
|
defaultAuthorizedKeys ++ cfg.extraAuthorizedKeys;
|
||||||
|
|
||||||
|
programs.ssh.knownHosts = knownHosts;
|
||||||
|
|
||||||
|
home-manager.users.${user}.programs.ssh = {
|
||||||
|
enable = true;
|
||||||
|
enableDefaultConfig = false;
|
||||||
|
settings =
|
||||||
|
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
||||||
|
// cfg.extraSettings;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# A machine with its own identity restores its host keys from secrets.
|
(mkIf cfg.hostKeys.restore {
|
||||||
(lib.mkIf cfg.hostKeys.restore {
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
@@ -128,42 +307,27 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# The daemon reads its host keys once at startup, so a re-key has to
|
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||||
# restart it to take effect.
|
|
||||||
sops.secrets = lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
|
||||||
inherit (cfg.hostKeys) sopsFile;
|
inherit (cfg.hostKeys) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "sshd.service" ];
|
restartUnits = [ "sshd.service" ];
|
||||||
});
|
});
|
||||||
|
|
||||||
# An empty list is what stops the daemon generating keys of its own.
|
|
||||||
services.openssh.hostKeys = [ ];
|
services.openssh.hostKeys = [ ];
|
||||||
services.openssh.extraConfig = lib.concatMapStrings (
|
services.openssh.extraConfig = concatStringsSep "" (
|
||||||
type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n"
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
||||||
) cfg.hostKeys.types;
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
# The client key the primary user offers to remote servers, present only on
|
(mkIf (cfg.userKey.sopsFile != null) {
|
||||||
# a machine that has one.
|
|
||||||
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
|
||||||
# The primary user is the only account that authenticates with this key,
|
|
||||||
# and the mode admits no other.
|
|
||||||
# The client rereads it per connection, so no unit restarts on a re-key.
|
|
||||||
sops.secrets.${userKeySecret} = {
|
sops.secrets.${userKeySecret} = {
|
||||||
inherit (cfg.userKey) sopsFile;
|
inherit (cfg.userKey) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = user;
|
owner = user;
|
||||||
};
|
};
|
||||||
|
|
||||||
# The client reads the decrypted key where it is written, so no copy of it
|
home-manager.users.${user}.programs.ssh.settings."*".IdentityFile =
|
||||||
# lives in the user's home to drift from the secret.
|
config.sops.secrets.${userKeySecret}.path;
|
||||||
# Declaring no defaults of home-manager's own leaves every other directive
|
|
||||||
# at the one OpenSSH itself ships.
|
|
||||||
home-manager.users.${user}.programs.ssh = {
|
|
||||||
enable = true;
|
|
||||||
enableDefaultConfig = false;
|
|
||||||
settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
27
secrets/pikachu.yaml
Normal file
27
secrets/pikachu.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
ssh-host-ed25519-key: ENC[AES256_GCM,data:nxj7nMDzeqr0CitIjns9ugFh7Sur0pVpBr8SgWbxQMpIeNvBp+ugmnAxaUq2HRoYCo68uh/YKJZgMCBPSWrEyIhUX0rXmiZFmusrviJSXrYmy+n2XvLPjuv4vPbJEfjudRdXkHPazB3JJbeVRhGgK7y7smhNZlr8lwPGo0Ui0jVqWB4MPQRP6dLKVfV5shIusOqdhg7y2JSkZLYIjOBOJjknCxGmr5+vF2qwpDJQWEyzuJv2QJ48IlU7QyHF76//6xOchteL70/lyXI4gLiGl2fEczx3KKIXE+DNetKVAQxh4QHwzz9JK8qIJMYVry2qFdGGgF8eN7YEJYA60r1pL++1l8Uq++C1Bsc9jCO91ayQb+spXK0uM3G/ZJka7brRQbvcr54MJEyH6+0uBscXw/wMJ1EBX3Np6dumQhKuALnsAIXE89bKJg1wyPwaCy8uMZMcjQhvNkG0FPSBatHz2bB7bnNuNiXyT+scYyN0huE4Ccq7/H3bHWqbipV51YBH/XZKmjDiCwu3i0t5MHmp,iv:9P6Pa3hBe0/jbacjvzV3VGJJ4+OLRarEJEkLMOftqPw=,tag:zA6ewbj2ovP9+2WcAXa9qA==,type:str]
|
||||||
|
ssh-host-rsa-key: ENC[AES256_GCM,data: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,iv:vfjzz6U6ey9wGs6Ia3Vd5HaGBvpSq4akp4e3eLC/A3M=,tag:ar1+i09oOEusEuQnr44ASw==,type:str]
|
||||||
|
ssh-user-ed25519-key: ENC[AES256_GCM,data:uHZADD2tq8vqlOAOWYniEnj1sl2IUqepf4YvrmRX/0nTksVnoethW6Yb2VNTgu5FsKCxvO65qMZ9KYupDkw/+SCZP2VH4TyhByQSr4SYxrEiNOJLdvmL2VnOWlNNZKaswgBePQR7TBG4mSIiTXCjEAcxeHAhBxLPnwiFxK/Zd6yQhq61x1N+ZtOj0q0QpADP3vioUohdkE5ODoEOUUoQJkYpjdJl2j4PhjCq7yHMEZpeMkqjZhVX5+i8blBu43BM4bKS2tBIJW2IvPV9sHOH8/tsR6p5q0pnpcJAOyOquBSo+eR0egY+liXjSv+Vo/ZDPapl7rKs2SoJ5huUGFbWjxbXY2+t2SYyiVA8lufrVUu94RCwOOJ8UyGYzAGGrChUImVDBsgk/GWwpOvWxcqydF8d6mDdxK9Iek9HtYsS+IKXLrF8GAI9OLB9jKgHVZgyQYSnC146cwCLt99TOFVocE1lfzwpFQTwKvxkep5jBEWiyqRaRUngti0JuF0ih1BWo+LKqxIqq9+V3I39xM8PfTjgHiifnxj4wuhS,iv:32zDKpNPYMiP6Rx7L9TAhxrMwcmphIGni+0jcESoLqg=,tag:YWGE62WQNm2Cer+eG+S5Xg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyTTVLWHVaZ1NxbGEzcVBq
|
||||||
|
MmorSkhzQ2hSV01idWE4VXNyNkNvMmpkTHpzCnFDQ0l3a1o1OWhmdE9WTjlob3p4
|
||||||
|
Y3BzMlJmeks4ZllzbEY1MkVqRCtrNjgKLS0tIFVhQXpDRjRUSEViMUhPM0V2RGRt
|
||||||
|
NkRpV05YV2RaTGozTUNNM3U5dmJ3dWsKSzkXmVyPhwN58SjMYYL/YbPoHYtZ9goF
|
||||||
|
VYRLRnfU298/3R/1nLNqTlMk4MYQ6NaGf2jOYTyObGgJVccOHF6wig==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGMDBPcHZ3YithYm5sM2Vh
|
||||||
|
NzJUVmpuT1FRdmw1bzI0a2p5NXFlZlRQN25NCmduQ1NKREVZT3Q5d0l4ZC9ZVUFU
|
||||||
|
all2UUZUMjZyaGxGMm5UOEQwcnNKS2sKLS0tIExPZmR5K05WZHRYRHZEeW1PRWhV
|
||||||
|
ZTZzTlhnYURJTDROTFpUTmFKc3ZkeXMKEWUCkVeHEt/Ay0lyAVjsqtLbu+pJOTJ5
|
||||||
|
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
|
lastmodified: "2026-08-03T01:26:32Z"
|
||||||
|
mac: ENC[AES256_GCM,data:w4i6+dwN/t/SvFTrP/YiapxOY6ecT7+cJ0v/5lYMe4m1vL2w4mJw9ZrxWifcCcTEfaa766Wc1NE+GxGrBZtlORExbtQCa4tg2GuIQCRqTEc5WSqhYrgMb4elWuYe+r3/SlAf1ldNxz+cmLVtbDpSs96bvkGsKdw/i+q9n2URJYE=,iv:MFWgMtIdpn1v0T6FPhTgBMxi+6kzf2ZAnOGaisRT0d0=,tag:qBX32/zUHSPagXU+u8G8tA==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.3
|
||||||
@@ -3,22 +3,31 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVYzZuaERsRjMyaTAwL3Ri
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvdGQ0ZnZ2UlVaTFVuRmJI
|
||||||
b2RhaGZ1aHNOSzVMamVxWkdKb3VKTk9QMmk4CmV0R3hYN3hkMU1tVDJLNkFlT08y
|
aVZNVjZVNGdiVGVhUTVXN3luU0V1eUZFb0ZRCkVrY3lKRE9GTUEzV0ZjUnk4UHdl
|
||||||
SUdUeUZ4d2JwNmdyOWVJcmZNcEtCb1EKLS0tIHhDV1NZWWdDZUNMYjVqYUVlc0ty
|
QnFMVDhMcGVUTWFQemxrSnNEMStpSEEKLS0tIFFIQndkZHp1NkRmRlB4RXlwdk5C
|
||||||
Y1owUFZPMXBHbDhjVWxTUjZGRk1IUzQK7VENq6TjuOFlon+CJqUxbIJZ9qka78C/
|
cTJVQW9iMWhaMlk1dUxBK1ZvQTRuV0kKm7/z24q4NcDFlVuxZViDFlJodjRzRqhY
|
||||||
LDsgaTD+7zCBPgASwPbF88pH6tdK7bvNLJnznlZdZBL12eOy25BmOQ==
|
7X9LqouIXcGhDgwq0hh+JXRfYCz9LDiUJtOLHR7Lu/oscCBCnk7N6Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
recipient: age1m0pk94ysjlw3lmf6pyuv5l5pepvdjss8w0vxjv90dq6ndp02tdgsdwdvue
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHY0ZXT3lRWS9DMFA1MHhl
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTOENXYmRRNmVQeUtvOVFy
|
||||||
MDZiWHhEMy9INGtpd1ZOdzh0OFRoUlZDa0hZCkJwTUV4c01YWlE1QjNDd3pRN3F0
|
eElWVUhnUnJDTk93aVJCVmpTcHd2aVpKSFhJCk9oUk5ObHNSZHNwSisrSE9JbjRI
|
||||||
SGJWWmFTT1NMQktNejVHY1RrRlZJNFEKLS0tIHlRZG9ZV3FrQktSN2tURVV1NmlW
|
WVllODlYek1VMDZpMmh3M2JRTU92ZEkKLS0tIFVVNG9RNnlzS0ZvMUM3bVN2Mldo
|
||||||
UTBZbFlqMmFGZ0VPSlA1dmNMU2Q3TFUKtL2V8t9+Qw5vjXursvCVRatflX8JKXJr
|
N0MvcXEraDcwUHVxbTJqWGdxTGhOVG8KIhIY9QGbt/eWy9bfST4tEkjLQLylaHRm
|
||||||
VuA8oe0nKpk7wh4fCzcT7RoRKpJY0gPFjIzeTZGVfoAmZIUWMhzRuw==
|
AwYIwU1Hw6HXR1TX3t0YciI3c8HcWISruY/tBR3xIHIdBlQR5OTaeA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
recipient: age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ODgyNjQ1RFMwOGpUaG5U
|
||||||
|
UDNjTENrYzY5UFVTNlRONlBhNmVUUXk2OVd3ClFQUUhFaUM4YmR6dVNlMXRwWUFY
|
||||||
|
ais3L0FyQ1ozWjFMMHhjWmU1NU5JdVUKLS0tIG1tS1NoZjhBb3JRak9XdEpOT3Vy
|
||||||
|
Uk5HdUd6NUsrZWx2Y1lrWGdHRXcxZEUK470gSumRCpgYvIWJcmylw0VTgyV3et/B
|
||||||
|
QkVLBz5x+ShVun27nN3oz8And0qXfwgXojhM3yWnBSUa9CFBsbTOJQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
lastmodified: "2026-07-20T03:22:00Z"
|
lastmodified: "2026-07-20T03:22:00Z"
|
||||||
mac: ENC[AES256_GCM,data:ei7PKVAIjJ6fGkxqJFc5wdYapq1gElel3fTJ+yKhvWHU+39aKcllG66T3d9FitRztgyt69phykHdKvxDHRUwYeyl1YBzyf1ZpPU5mXJb+hkLtVB1Am7StcP+m7jFqKSmqtYhIT9OxUrH0MJ8qeoU9216otwkhhpPz2hr1s7KYFk=,iv:Pp03KmlinjJiiTZezr0LzzkcHb1a5XWgDpu38jhl9Rk=,tag:HAqFitge+KTCtDE24t8/ig==,type:str]
|
mac: ENC[AES256_GCM,data:ei7PKVAIjJ6fGkxqJFc5wdYapq1gElel3fTJ+yKhvWHU+39aKcllG66T3d9FitRztgyt69phykHdKvxDHRUwYeyl1YBzyf1ZpPU5mXJb+hkLtVB1Am7StcP+m7jFqKSmqtYhIT9OxUrH0MJ8qeoU9216otwkhhpPz2hr1s7KYFk=,iv:Pp03KmlinjJiiTZezr0LzzkcHb1a5XWgDpu38jhl9Rk=,tag:HAqFitge+KTCtDE24t8/ig==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
Reference in New Issue
Block a user