Compare commits
1 Commits
feat/p0-li
...
fix/fleet-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7b184b0e2 |
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Mosaic Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -23,6 +23,5 @@
|
|||||||
"turbo": "^2.0.0",
|
"turbo": "^2.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
},
|
}
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Mosaic Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
# Run `load_credentials --help` for details.
|
# Run `load_credentials --help` for details.
|
||||||
|
|
||||||
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
for _cand in "$HOME/.config/mosaic/credentials.json"; do
|
for _cand in "$HOME/.config/mosaic/credentials.json" "$HOME/src/jarvis-brain/credentials.json"; do
|
||||||
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
||||||
done
|
done
|
||||||
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/.config/mosaic/credentials.json}"
|
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/src/jarvis-brain/credentials.json}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_mosaic_require_jq() {
|
_mosaic_require_jq() {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ gitea_url_matches_host() {
|
|||||||
|
|
||||||
get_gitea_service_for_host() {
|
get_gitea_service_for_host() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
local cred_file="${MOSAIC_CREDENTIALS_FILE:-$HOME/.config/mosaic/credentials.json}"
|
local cred_file="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||||
|
|
||||||
case "$host" in
|
case "$host" in
|
||||||
git.mosaicstack.dev)
|
git.mosaicstack.dev)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
|||||||
FORMAT="table"
|
FORMAT="table"
|
||||||
SINGLE_SERVICE=""
|
SINGLE_SERVICE=""
|
||||||
QUIET=false
|
QUIET=false
|
||||||
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/.config/mosaic/credentials.json}"
|
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||||
|
|
||||||
while getopts "f:s:qh" opt; do
|
while getopts "f:s:qh" opt; do
|
||||||
case $opt in
|
case $opt in
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ FILE_PATH="${FILE_PATH/#\~/$HOME}"
|
|||||||
# Block writes to Claude Code auto-memory files
|
# Block writes to Claude Code auto-memory files
|
||||||
if [[ "$FILE_PATH" =~ /.claude/projects/.+/memory/.*\.md$ ]]; then
|
if [[ "$FILE_PATH" =~ /.claude/projects/.+/memory/.*\.md$ ]]; then
|
||||||
echo "BLOCKED: Do not write agent learnings to ~/.claude/projects/*/memory/ — this is a runtime-specific silo."
|
echo "BLOCKED: Do not write agent learnings to ~/.claude/projects/*/memory/ — this is a runtime-specific silo."
|
||||||
if [[ -n "${OPENBRAIN_URL:-}" ]]; then
|
echo "Use OpenBrain instead: MCP 'capture' tool or REST POST https://brain.woltje.com/v1/thoughts"
|
||||||
echo "Use OpenBrain instead: MCP 'capture' tool or REST POST ${OPENBRAIN_URL%/}/v1/thoughts"
|
|
||||||
else
|
|
||||||
echo "Use OpenBrain instead: the 'capture' MCP tool (set OPENBRAIN_URL for the REST endpoint)."
|
|
||||||
fi
|
|
||||||
echo "File blocked: $FILE_PATH"
|
echo "File blocked: $FILE_PATH"
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.34",
|
"version": "0.0.31",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
@@ -63,6 +63,5 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"framework"
|
"framework"
|
||||||
],
|
]
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
getDefaultOperatorSourceLabel,
|
getDefaultOperatorSourceLabel,
|
||||||
getRosterAgent,
|
getRosterAgent,
|
||||||
loadFleetRoster,
|
loadFleetRoster,
|
||||||
mergeAgentEnv,
|
|
||||||
registerFleetCommand,
|
registerFleetCommand,
|
||||||
resolveFleetPaths,
|
resolveFleetPaths,
|
||||||
type CommandRunner,
|
type CommandRunner,
|
||||||
@@ -122,37 +121,6 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
|
||||||
const generated = [
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
|
||||||
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
const existing = [
|
|
||||||
'MOSAIC_AGENT_NAME=old-name',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=old-runtime',
|
|
||||||
'MOSAIC_AGENT_WORKDIR=/srv/old',
|
|
||||||
'MOSAIC_TMUX_SOCKET=old-socket',
|
|
||||||
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
|
||||||
'# site note',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
expect(mergeAgentEnv(generated, existing)).toBe(
|
|
||||||
[
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'MOSAIC_AGENT_WORKDIR=/srv/new',
|
|
||||||
'MOSAIC_TMUX_SOCKET=mosaic-factory',
|
|
||||||
'MOSAIC_AGENT_COMMAND=/home/jarvis/.config/mosaic/fleet/canary.sh',
|
|
||||||
'# site note',
|
|
||||||
'',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
||||||
cleanup = await tempDir();
|
cleanup = await tempDir();
|
||||||
const rosterPath = join(cleanup, 'roster.yaml');
|
const rosterPath = join(cleanup, 'roster.yaml');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import { access, chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { homedir, hostname } from 'node:os';
|
import { homedir, hostname } from 'node:os';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -148,29 +148,6 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeAgentEnv(generatedEnv: string, existingEnv?: string): string {
|
|
||||||
if (!existingEnv?.trim()) {
|
|
||||||
return generatedEnv;
|
|
||||||
}
|
|
||||||
const generatedKeys = new Set(
|
|
||||||
generatedEnv
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1])
|
|
||||||
.filter((key): key is string => key !== undefined),
|
|
||||||
);
|
|
||||||
const preservedLines = existingEnv.split('\n').filter((line) => {
|
|
||||||
if (!line.trim()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const key = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
|
|
||||||
return key === undefined || !generatedKeys.has(key);
|
|
||||||
});
|
|
||||||
if (preservedLines.length === 0) {
|
|
||||||
return generatedEnv;
|
|
||||||
}
|
|
||||||
return [generatedEnv.trimEnd(), ...preservedLines, ''].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
export function buildFleetServiceCommand(action: FleetServiceAction, agentName?: string): string[] {
|
||||||
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
const service = agentName ? `mosaic-agent@${agentName}.service` : 'mosaic-tmux-holder.service';
|
||||||
return ['systemctl', '--user', action, service];
|
return ['systemctl', '--user', action, service];
|
||||||
@@ -478,19 +455,18 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
|
|||||||
await mkdir(activePaths.systemdUserDir, { recursive: true });
|
await mkdir(activePaths.systemdUserDir, { recursive: true });
|
||||||
await mkdir(activePaths.agentEnvDir, { recursive: true });
|
await mkdir(activePaths.agentEnvDir, { recursive: true });
|
||||||
|
|
||||||
const startAgentSessionPath = join(activePaths.fleetToolsDir, 'start-agent-session.sh');
|
|
||||||
const sendMessagePath = join(activePaths.tmuxToolsDir, 'send-message.sh');
|
|
||||||
const agentSendPath = join(activePaths.tmuxToolsDir, 'agent-send.sh');
|
|
||||||
const executableToolPaths = [startAgentSessionPath, sendMessagePath, agentSendPath];
|
|
||||||
await copyFile(
|
await copyFile(
|
||||||
join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'),
|
join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'),
|
||||||
startAgentSessionPath,
|
join(activePaths.fleetToolsDir, 'start-agent-session.sh'),
|
||||||
|
);
|
||||||
|
await copyFile(
|
||||||
|
join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'),
|
||||||
|
join(activePaths.tmuxToolsDir, 'send-message.sh'),
|
||||||
|
);
|
||||||
|
await copyFile(
|
||||||
|
join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'),
|
||||||
|
join(activePaths.tmuxToolsDir, 'agent-send.sh'),
|
||||||
);
|
);
|
||||||
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'), sendMessagePath);
|
|
||||||
await copyFile(join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'), agentSendPath);
|
|
||||||
for (const toolPath of executableToolPaths) {
|
|
||||||
await chmod(toolPath, 0o755);
|
|
||||||
}
|
|
||||||
await copyFile(
|
await copyFile(
|
||||||
join(frameworkRoot, 'systemd', 'user', 'mosaic-tmux-holder.service'),
|
join(frameworkRoot, 'systemd', 'user', 'mosaic-tmux-holder.service'),
|
||||||
join(activePaths.systemdUserDir, 'mosaic-tmux-holder.service'),
|
join(activePaths.systemdUserDir, 'mosaic-tmux-holder.service'),
|
||||||
@@ -501,9 +477,10 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const agent of roster.agents) {
|
for (const agent of roster.agents) {
|
||||||
const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`);
|
await writeFile(
|
||||||
const existingEnv = (await canRead(envPath)) ? await readFile(envPath, 'utf8') : undefined;
|
join(activePaths.agentEnvDir, `${agent.name}.env`),
|
||||||
await writeFile(envPath, mergeAgentEnv(generateAgentEnv(roster, agent), existingEnv));
|
generateAgentEnv(roster, agent),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
console.log(`Installed fleet files for ${roster.agents.length} agent(s).`);
|
||||||
|
|||||||
Reference in New Issue
Block a user