@@ -10,4 +10,7 @@ set -eu
|
|||||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||||
|
|
||||||
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
|
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
|
||||||
|
# Deterministic plumbing evidence: which MOSAIC_* variables did the
|
||||||
|
# orchestrator actually deliver? (Auth secrets are not MOSAIC_-prefixed.)
|
||||||
|
(env | grep '^MOSAIC_' | sort) >&2 2>/dev/null || true
|
||||||
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
|
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
|
||||||
|
|||||||
+12
-2
@@ -12,11 +12,21 @@ set -eu
|
|||||||
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
|
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
|
||||||
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
|
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
|
||||||
|
|
||||||
|
# Workspace (M5): run inside the provided workspace when present.
|
||||||
|
if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
|
||||||
|
mkdir -p "$MOSAIC_WORKSPACE"
|
||||||
|
cd "$MOSAIC_WORKSPACE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Capabilities (M5): explicit allowlist or no tools.
|
||||||
|
TOOLS_FLAG="--no-tools"
|
||||||
|
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
|
||||||
|
|
||||||
# All flags documented in the pi package README (CLI Reference):
|
# All flags documented in the pi package README (CLI Reference):
|
||||||
# -p/--print noninteractive: print the response and exit
|
# -p/--print noninteractive: print the response and exit
|
||||||
# --system-prompt replace the default prompt with the generated one
|
# --system-prompt replace the default prompt with the generated one
|
||||||
# --no-* no ambient context/skills/extensions/templates/themes
|
# --no-* no ambient context/skills/extensions/templates/themes
|
||||||
# --no-session ephemeral; --no-tools this runtime needs no tools
|
# --no-session ephemeral; TOOLS_FLAG per capabilities
|
||||||
# --offline no startup network operations (update checks/telemetry)
|
# --offline no startup network operations (update checks/telemetry)
|
||||||
exec pi \
|
exec pi \
|
||||||
--offline \
|
--offline \
|
||||||
@@ -26,7 +36,7 @@ exec pi \
|
|||||||
--no-prompt-templates \
|
--no-prompt-templates \
|
||||||
--no-themes \
|
--no-themes \
|
||||||
--no-context-files \
|
--no-context-files \
|
||||||
--no-tools \
|
$TOOLS_FLAG \
|
||||||
--provider "$PI_PROVIDER" \
|
--provider "$PI_PROVIDER" \
|
||||||
--model "$PI_MODEL" \
|
--model "$PI_MODEL" \
|
||||||
--system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \
|
--system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ services:
|
|||||||
# Mission directives injection point (set by the task runner when the
|
# Mission directives injection point (set by the task runner when the
|
||||||
# task references a mission; container path of the run snapshot)
|
# task references a mission; container path of the run snapshot)
|
||||||
MOSAIC_MISSION_FILE: ${MOSAIC_MISSION_FILE:-}
|
MOSAIC_MISSION_FILE: ${MOSAIC_MISSION_FILE:-}
|
||||||
|
# Workspace + capabilities (set by the task runner; M5)
|
||||||
|
MOSAIC_WORKSPACE: ${MOSAIC_WORKSPACE:-}
|
||||||
|
MOSAIC_TOOLS: ${MOSAIC_TOOLS:-}
|
||||||
# mock adapter only: verbatim response for deterministic seam tests
|
# mock adapter only: verbatim response for deterministic seam tests
|
||||||
MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-}
|
MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-}
|
||||||
# Documented container auth alternative: provider API key via
|
# Documented container auth alternative: provider API key via
|
||||||
|
|||||||
+51
-1
@@ -38,6 +38,7 @@ const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|||||||
const RUNS_DIRNAME = "runs";
|
const RUNS_DIRNAME = "runs";
|
||||||
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
||||||
const DEFAULT_TIMEOUT_SECONDS = 120;
|
const DEFAULT_TIMEOUT_SECONDS = 120;
|
||||||
|
const SUPPORTED_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls"]; // pi documented built-ins
|
||||||
|
|
||||||
function fail(exitCode, message) {
|
function fail(exitCode, message) {
|
||||||
process.stderr.write(`mosaic-task: ${message}\n`);
|
process.stderr.write(`mosaic-task: ${message}\n`);
|
||||||
@@ -105,7 +106,7 @@ function validateMission(document, file) {
|
|||||||
|
|
||||||
function validateTask(document, file) {
|
function validateTask(document, file) {
|
||||||
if (!isPlainObject(document)) fail(2, "task must be a JSON object");
|
if (!isPlainObject(document)) fail(2, "task must be a JSON object");
|
||||||
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds"], "task");
|
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities"], "task");
|
||||||
if (document.taskVersion !== 1) {
|
if (document.taskVersion !== 1) {
|
||||||
fail(2, `unsupported taskVersion: ${JSON.stringify(document.taskVersion)} (supported: 1)`);
|
fail(2, `unsupported taskVersion: ${JSON.stringify(document.taskVersion)} (supported: 1)`);
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,39 @@ function validateTask(document, file) {
|
|||||||
timeoutSeconds = document.timeoutSeconds;
|
timeoutSeconds = document.timeoutSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workspace (M5): absent = none; ":run" = ephemeral per-run; otherwise a
|
||||||
|
// persistent named workspace under <dataRoot>/workspaces/<name>.
|
||||||
|
let workspace = null;
|
||||||
|
if (document.workspace !== undefined && document.workspace !== null) {
|
||||||
|
if (typeof document.workspace !== "string" || document.workspace.length === 0) {
|
||||||
|
fail(2, 'task "workspace" must be a non-empty string when present');
|
||||||
|
}
|
||||||
|
if (document.workspace !== ":run") {
|
||||||
|
validateId(document.workspace, "task workspace");
|
||||||
|
}
|
||||||
|
workspace = document.workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities (M5): optional tools allowlist mapped by adapters to their
|
||||||
|
// native permission flags. Absent = no tools.
|
||||||
|
let tools = null;
|
||||||
|
if (document.capabilities !== undefined && document.capabilities !== null) {
|
||||||
|
if (!isPlainObject(document.capabilities)) fail(2, '"capabilities" must be a JSON object');
|
||||||
|
rejectUnknownKeys(document.capabilities, ["tools"], '"capabilities"');
|
||||||
|
if (!Array.isArray(document.capabilities.tools) || document.capabilities.tools.length === 0) {
|
||||||
|
fail(2, '"capabilities.tools" must be a non-empty array of tool names');
|
||||||
|
}
|
||||||
|
const seen = new Set();
|
||||||
|
for (const tool of document.capabilities.tools) {
|
||||||
|
if (!SUPPORTED_TOOLS.includes(tool)) {
|
||||||
|
fail(2, `unsupported tool: ${JSON.stringify(tool)} (supported: ${SUPPORTED_TOOLS.join(", ")})`);
|
||||||
|
}
|
||||||
|
if (seen.has(tool)) fail(2, `duplicate tool in capabilities.tools: ${tool}`);
|
||||||
|
seen.add(tool);
|
||||||
|
}
|
||||||
|
tools = [...seen];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taskVersion: document.taskVersion,
|
taskVersion: document.taskVersion,
|
||||||
id: document.id,
|
id: document.id,
|
||||||
@@ -153,6 +187,8 @@ function validateTask(document, file) {
|
|||||||
missionSnapshot,
|
missionSnapshot,
|
||||||
expectExact,
|
expectExact,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
|
workspace,
|
||||||
|
tools,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +259,18 @@ function runTask(taskFile) {
|
|||||||
spawnEnv.MOSAIC_MISSION_FILE = `/var/lib/mosaic/${relative.split(path.sep).join("/")}/mission.json`;
|
spawnEnv.MOSAIC_MISSION_FILE = `/var/lib/mosaic/${relative.split(path.sep).join("/")}/mission.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workspace (M5): create host-side, pass the CONTAINER path.
|
||||||
|
let workspaceContainerPath = null;
|
||||||
|
if (task.workspace === ":run") {
|
||||||
|
fs.mkdirSync(path.join(runDir, "workspace"), { recursive: true });
|
||||||
|
workspaceContainerPath = `/var/lib/mosaic/runs/${runId}/workspace`;
|
||||||
|
} else if (task.workspace) {
|
||||||
|
fs.mkdirSync(path.join(resolved.dataRoot, "workspaces", task.workspace), { recursive: true });
|
||||||
|
workspaceContainerPath = `/var/lib/mosaic/workspaces/${task.workspace}`;
|
||||||
|
}
|
||||||
|
if (workspaceContainerPath) spawnEnv.MOSAIC_WORKSPACE = workspaceContainerPath;
|
||||||
|
spawnEnv.MOSAIC_TOOLS = task.tools ? task.tools.join(",") : "";
|
||||||
|
|
||||||
const proc = spawnSync(
|
const proc = spawnSync(
|
||||||
"docker",
|
"docker",
|
||||||
["compose", "run", "--rm", "-T", "mosaic-agent", task.prompt],
|
["compose", "run", "--rm", "-T", "mosaic-agent", task.prompt],
|
||||||
@@ -269,6 +317,8 @@ function runTask(taskFile) {
|
|||||||
request: task.prompt,
|
request: task.prompt,
|
||||||
response,
|
response,
|
||||||
expectedExact: expected,
|
expectedExact: expected,
|
||||||
|
workspace: task.workspace,
|
||||||
|
tools: task.tools,
|
||||||
exitCode: proc.status,
|
exitCode: proc.status,
|
||||||
signal: proc.signal ?? null,
|
signal: proc.signal ?? null,
|
||||||
provider: resolved.execution.provider,
|
provider: resolved.execution.provider,
|
||||||
|
|||||||
@@ -146,6 +146,38 @@ else
|
|||||||
echo "skip adapter seam cases (docker daemon unavailable)"
|
echo "skip adapter seam cases (docker daemon unavailable)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---------- workspace + capabilities (M5): deterministic mock cases ----------
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
printf '{"taskVersion":1,"id":"t-ws","prompt":"ignored","workspace":"suitews","capabilities":{"tools":["read","bash"]}}' > "$SANDBOX/ws-task.json"
|
||||||
|
expect_exit "workspace+tools task runs via mock" 0 -- \
|
||||||
|
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \
|
||||||
|
scripts/run-task.sh run "$SANDBOX/ws-task.json"
|
||||||
|
WSL="$(ls -dt "$SANDBOX/data/runs"/r-* 2>/dev/null | head -1)"
|
||||||
|
grep -q '^MOSAIC_WORKSPACE=/var/lib/mosaic/workspaces/suitews$' "$WSL/stderr.txt" 2>/dev/null \
|
||||||
|
&& grep -q '^MOSAIC_TOOLS=read,bash$' "$WSL/stderr.txt" 2>/dev/null \
|
||||||
|
&& check "workspace path + tools delivered to adapter" 0 \
|
||||||
|
|| check "workspace path + tools delivered to adapter" 1
|
||||||
|
[ -d "$SANDBOX/data/workspaces/suitews" ] \
|
||||||
|
&& check "persistent workspace created on host" 0 \
|
||||||
|
|| check "persistent workspace created on host" 1
|
||||||
|
|
||||||
|
expect_exit "plain task still runs (no workspace/tools)" 0 -- \
|
||||||
|
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK \
|
||||||
|
scripts/run-task.sh run "$SANDBOX/ok.json"
|
||||||
|
PL="$(ls -dt "$SANDBOX/data/runs"/r-* 2>/dev/null | head -1)"
|
||||||
|
grep -q '^MOSAIC_WORKSPACE=$' "$PL/stderr.txt" 2>/dev/null \
|
||||||
|
&& check "workspace var present but empty when absent" 0 || check "workspace var present but empty when absent" 1
|
||||||
|
grep -q '^MOSAIC_TOOLS=$' "$PL/stderr.txt" 2>/dev/null \
|
||||||
|
&& check "tools empty when absent" 0 || check "tools empty when absent" 1
|
||||||
|
|
||||||
|
printf '{"taskVersion":1,"id":"t-badtool","prompt":"x","capabilities":{"tools":["sudo"]}}' > "$SANDBOX/badtool.json"
|
||||||
|
expect_exit "unknown tool exits 2" 2 -- $TASK validate "$SANDBOX/badtool.json"
|
||||||
|
printf '{"taskVersion":1,"id":"t-badws","prompt":"x","workspace":"../escape"}' > "$SANDBOX/badws.json"
|
||||||
|
expect_exit "workspace traversal exits 2" 2 -- $TASK validate "$SANDBOX/badws.json"
|
||||||
|
else
|
||||||
|
echo "skip workspace/capability cases (docker daemon unavailable)"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------- live: real runs (Docker + credentials required) ----------
|
# ---------- live: real runs (Docker + credentials required) ----------
|
||||||
# On failure, surface the run record + agent stderr BEFORE the sandbox
|
# On failure, surface the run record + agent stderr BEFORE the sandbox
|
||||||
# cleanup destroys them. Never let a wrong-exit mask the real reason.
|
# cleanup destroys them. Never let a wrong-exit mask the real reason.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"taskVersion": 1,
|
||||||
|
"id": "t-workspace-demo",
|
||||||
|
"prompt": "Use the bash tool to create a file named proof.txt in the current directory containing exactly the text: workspace works. Then reply with exactly: WORKSPACE_OK",
|
||||||
|
"workspace": "demo",
|
||||||
|
"capabilities": { "tools": ["bash", "read", "write"] },
|
||||||
|
"expectExact": "WORKSPACE_OK",
|
||||||
|
"timeoutSeconds": 180
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user