m4: add local agent loop smoke

This commit is contained in:
HWLAB Code Queue
2026-05-21 15:49:53 +00:00
parent e327a90e6c
commit 1e8d009e95
4 changed files with 288 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# M4 Agent Automation Loop Local Contract Smoke
Run the local M4 smoke from the repository root:
```sh
node scripts/m4-agent-loop-smoke.mjs
```
This check stays local. It uses the `internal/agent` runtime skeleton and a
repo fixture to verify:
- lifecycle coverage for `create`, `start`, `trace`, `finish`, and `cleanup`;
- workspace isolation per agent session;
- explicit skills `commitId` wiring;
- evidence record fields, URI shape, and SHA-256 digest;
- the fixture evidence note that marks the run as local-only.
## Real DEV Preconditions
A real DEV M4 run is separate from this smoke. It would need a live DEV
environment, a seeded `prj_*` project, the `hwlab-agent-mgr`,
`hwlab-agent-worker`, and `hwlab-agent-skills` services, and a stable
`hwlab-agent-skills` artifact with an explicit `commitId`.
That live path must not read tokens or secrets from the runner. Do not inspect
`GH_TOKEN`, `GITHUB_TOKEN`, kubeconfig material, tunnel credentials, or any
other secret-bearing source as part of this smoke. If a live M4 path cannot run
without those reads, the blocker is safety, not contract shape.
## Failure Classification
- `contract_blocker`: fixture, lifecycle, or field shape mismatch.
- `environment_blocker`: DEV-only boundary or endpoint assumptions are wrong.
- `agent_blocker`: workspace isolation, cleanup, or evidence wiring fails.
- `observability_blocker`: a live artifact cannot be tied to service or
`commitId` identity.
- `safety_blocker`: any secret read, real deployment, or long-running agent
task.
## What This Is Not
This does not deploy, does not call production, and does not run a real agent
job. It only checks the local runtime skeleton contract and its fixture-backed
evidence note.
@@ -0,0 +1,11 @@
# M4 Agent Automation Loop Evidence Plan
- Scenario: m4-agent-automation-loop-local-contract-smoke
- Environment: dev
- Project: prj_m4_agent_loop
- Agent session: agt_m4_agent_loop_01
- Worker session: wkr_agt_m4_agent_loop_01
- Operation: op_agt_m4_agent_loop_01
- Evidence: evi_agt_m4_agent_loop_01
- Skill commitId: 6509a35
- Boundary: local only; no secrets, no live DEV agent run, no long-running task
+14
View File
@@ -0,0 +1,14 @@
{
"scenario": "m4-agent-automation-loop-local-contract-smoke",
"environment": "dev",
"projectId": "prj_m4_agent_loop",
"agentSessionId": "agt_m4_agent_loop_01",
"goal": "verify M4 agent automation loop skeleton",
"prompt": "local contract smoke for agent automation loop",
"skillCommitId": "6509a35",
"evidencePath": "fixtures/mvp/m4-agent-loop/evidence/m4-agent-loop-plan.md",
"workspaceIsolation": {
"projectId": "prj_m4_agent_loop_alt",
"agentSessionId": "agt_m4_agent_loop_02"
}
}
+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
AGENT_MGR_SERVICE_ID,
AGENT_SESSION_STATUSES,
AGENT_SKILLS_SERVICE_ID,
AGENT_WORKER_SERVICE_ID,
WORKER_SESSION_STATUSES,
createAgentRunPlan,
createSkillsManifest,
finalizeAgentRun
} from "../internal/agent/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixedNow = () => "2026-05-21T00:00:00.000Z";
async function readJSON(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
}
async function readText(relativePath) {
return readFile(path.join(repoRoot, relativePath), "utf8");
}
function sha256Hex(value) {
return createHash("sha256").update(String(value)).digest("hex");
}
function expectLifecycleConstants() {
assert.deepEqual(AGENT_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]);
assert.deepEqual(WORKER_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]);
}
function expectRunPlan(plan, fixture) {
const workerSessionId = `wkr_${fixture.agentSessionId}`;
const traceId = `trc_${fixture.agentSessionId}`;
const operationId = `op_${fixture.agentSessionId}`;
const evidenceId = `evi_${fixture.agentSessionId}`;
const workspaceVolumeId = `vol_${fixture.agentSessionId}`;
const mountPath = `/workspace/${fixture.projectId}/${fixture.agentSessionId}`;
const evidenceUri = `file://${path.posix.join(mountPath, "evidence.txt")}`;
assert.equal(plan.skillCommitId, fixture.skillCommitId);
assert.equal(plan.agentSession.serviceId, AGENT_MGR_SERVICE_ID);
assert.equal(plan.workerSession.serviceId, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.agentSession.agentSessionId, fixture.agentSessionId);
assert.equal(plan.agentSession.projectId, fixture.projectId);
assert.equal(plan.agentSession.environment, fixture.environment);
assert.equal(plan.agentSession.status, "start");
assert.equal(plan.agentSession.startedAt, fixedNow());
assert.equal(plan.agentSession.updatedAt, fixedNow());
assert.equal(plan.agentSession.metadata.currentState, "start");
assert.equal(plan.agentSession.metadata.workerSessionId, workerSessionId);
assert.equal(plan.agentSession.metadata.workspaceVolumeId, workspaceVolumeId);
assert.equal(plan.workerSession.workerSessionId, workerSessionId);
assert.equal(plan.workerSession.agentSessionId, fixture.agentSessionId);
assert.equal(plan.workerSession.projectId, fixture.projectId);
assert.equal(plan.workerSession.environment, fixture.environment);
assert.equal(plan.workerSession.status, "start");
assert.equal(plan.workerSession.startedAt, fixedNow());
assert.equal(plan.workerSession.updatedAt, fixedNow());
assert.equal(plan.workerSession.metadata.currentState, "start");
assert.equal(plan.workerSession.metadata.workspaceVolumeId, workspaceVolumeId);
assert.equal(plan.workspaceVolume.workspaceVolumeId, workspaceVolumeId);
assert.equal(plan.workspaceVolume.mountPath, mountPath);
assert.equal(plan.workspaceVolume.serviceId, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.workspaceVolume.lifecycle, "create");
assert.equal(plan.workspaceVolume.createdAt, fixedNow());
assert.equal(plan.workspaceVolume.updatedAt, fixedNow());
assert.deepEqual(
plan.events.map((event) => event.state),
["create", "start", "trace", "finish", "cleanup"]
);
assert.equal(plan.events[0].actor, AGENT_MGR_SERVICE_ID);
assert.equal(plan.events[0].agentSessionId, fixture.agentSessionId);
assert.equal(plan.events[1].actor, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.events[1].workerSessionId, workerSessionId);
assert.equal(plan.events[2].actor, AGENT_MGR_SERVICE_ID);
assert.equal(plan.events[2].traceEvent.traceEventId, `tev_${fixture.agentSessionId}`);
assert.equal(plan.events[2].traceEvent.traceId, traceId);
assert.equal(plan.events[2].traceEvent.projectId, fixture.projectId);
assert.equal(plan.events[2].traceEvent.serviceId, AGENT_MGR_SERVICE_ID);
assert.equal(plan.events[2].traceEvent.level, "info");
assert.equal(plan.events[2].traceEvent.message, fixture.prompt);
assert.equal(plan.events[2].traceEvent.environment, fixture.environment);
assert.equal(plan.events[2].traceEvent.agentSessionId, fixture.agentSessionId);
assert.equal(plan.events[2].traceEvent.workerSessionId, workerSessionId);
assert.equal(plan.events[2].traceEvent.operationId, operationId);
assert.deepEqual(plan.events[2].traceEvent.metadata, {});
assert.equal(plan.traceEvent.traceId, traceId);
assert.equal(plan.traceEvent.message, fixture.prompt);
assert.equal(plan.traceEvent.occurredAt, fixedNow());
assert.equal(plan.events[3].actor, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.events[3].evidenceRecord.evidenceId, evidenceId);
assert.equal(plan.events[4].actor, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.events[4].workspaceVolumeId, workspaceVolumeId);
assert.equal(plan.evidenceRecord.evidenceId, evidenceId);
assert.equal(plan.evidenceRecord.projectId, fixture.projectId);
assert.equal(plan.evidenceRecord.operationId, operationId);
assert.equal(plan.evidenceRecord.agentSessionId, fixture.agentSessionId);
assert.equal(plan.evidenceRecord.workerSessionId, workerSessionId);
assert.equal(plan.evidenceRecord.kind, "artifact");
assert.equal(plan.evidenceRecord.uri, evidenceUri);
assert.equal(plan.evidenceRecord.mimeType, "text/plain");
assert.equal(plan.evidenceRecord.sha256, sha256Hex(evidenceUri));
assert.equal(plan.evidenceRecord.sizeBytes, 0);
assert.equal(plan.evidenceRecord.serviceId, AGENT_WORKER_SERVICE_ID);
assert.equal(plan.evidenceRecord.environment, fixture.environment);
assert.equal(plan.evidenceRecord.createdAt, fixedNow());
assert.equal(plan.evidenceRecord.metadata.skillCommitId, fixture.skillCommitId);
assert.equal(plan.evidenceRecord.metadata.prompt, fixture.prompt);
assert.equal(plan.evidenceRecord.metadata.goal, fixture.goal);
assert.equal(plan.evidenceRecord.metadata.workspaceVolumeId, workspaceVolumeId);
}
function expectFinishedRun(finished, originalPlan) {
assert.equal(finished.agentSession.status, "cleanup");
assert.equal(finished.workerSession.status, "cleanup");
assert.equal(finished.agentSession.completedAt, fixedNow());
assert.equal(finished.workerSession.completedAt, fixedNow());
assert.equal(finished.agentSession.metadata.cleanedUp, true);
assert.equal(finished.workerSession.metadata.cleanedUp, true);
assert.equal(finished.cleanup.workspaceVolumeId, originalPlan.workspaceVolume.workspaceVolumeId);
assert.equal(finished.cleanup.removed, true);
assert.equal(finished.evidenceRecord.sha256, originalPlan.evidenceRecord.sha256);
assert.equal(originalPlan.agentSession.status, "start");
assert.equal(originalPlan.workerSession.status, "start");
}
function expectSkillsManifest(manifest, fixture) {
assert.equal(manifest.manifestVersion, "v1");
assert.equal(manifest.environment, fixture.environment);
assert.equal(manifest.commitId, fixture.skillCommitId);
assert.equal(manifest.namespace, "hwlab");
assert.equal(manifest.endpoint, "http://74.48.78.17:6667");
assert.equal(manifest.profiles.dev.enabled, true);
assert.equal(manifest.profiles.dev.namespace, "hwlab-dev");
assert.equal(manifest.services[0].serviceId, AGENT_SKILLS_SERVICE_ID);
assert.equal(manifest.services[0].image, `hwlab-agent-runtime:${fixture.skillCommitId.slice(0, 7)}`);
assert.equal(manifest.services[0].env.HWLAB_SKILLS_COMMIT_ID, fixture.skillCommitId);
assert.deepEqual(manifest.skills, []);
}
function expectWorkspaceIsolation(primaryPlan, secondaryPlan, fixture) {
const primaryVolumeId = primaryPlan.workspaceVolume.workspaceVolumeId;
const secondaryVolumeId = secondaryPlan.workspaceVolume.workspaceVolumeId;
const secondaryMountPath = `/workspace/${fixture.workspaceIsolation.projectId}/${fixture.workspaceIsolation.agentSessionId}`;
const secondaryEvidenceUri = `file://${path.posix.join(secondaryMountPath, "evidence.txt")}`;
assert.notEqual(primaryVolumeId, secondaryVolumeId);
assert.notEqual(primaryPlan.workspaceVolume.mountPath, secondaryPlan.workspaceVolume.mountPath);
assert.equal(secondaryPlan.workspaceVolume.mountPath, secondaryMountPath);
assert.equal(secondaryPlan.evidenceRecord.uri, secondaryEvidenceUri);
assert.equal(secondaryPlan.evidenceRecord.metadata.workspaceVolumeId, secondaryVolumeId);
assert.equal(secondaryPlan.traceEvent.traceId, `trc_${fixture.workspaceIsolation.agentSessionId}`);
}
function expectEvidenceFixture(text, fixture) {
assert.match(text, /^# M4 Agent Automation Loop Evidence Plan/m);
assert.ok(text.includes(`- Scenario: ${fixture.scenario}`));
assert.ok(text.includes(`- Environment: ${fixture.environment}`));
assert.ok(text.includes(`- Project: ${fixture.projectId}`));
assert.ok(text.includes(`- Skill commitId: ${fixture.skillCommitId}`));
assert.ok(text.includes("no secrets"));
assert.ok(text.includes("no live DEV agent run"));
}
const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json");
const evidenceFixture = await readText(fixture.evidencePath);
assert.equal(fixture.environment, "dev");
assert.equal(fixture.projectId.startsWith("prj_"), true);
assert.equal(fixture.agentSessionId.startsWith("agt_"), true);
assert.equal(fixture.skillCommitId.length >= 7, true);
assert.equal(fixture.workspaceIsolation.projectId.startsWith("prj_"), true);
assert.equal(fixture.workspaceIsolation.agentSessionId.startsWith("agt_"), true);
expectLifecycleConstants();
expectEvidenceFixture(evidenceFixture, fixture);
const plan = createAgentRunPlan({
agentSessionId: fixture.agentSessionId,
projectId: fixture.projectId,
goal: fixture.goal,
prompt: fixture.prompt,
skillCommitId: fixture.skillCommitId,
now: fixedNow
});
expectRunPlan(plan, fixture);
const finished = finalizeAgentRun(plan, { now: fixedNow });
expectFinishedRun(finished, plan);
const manifest = createSkillsManifest({
commitId: fixture.skillCommitId
});
expectSkillsManifest(manifest, fixture);
const isolationPlan = createAgentRunPlan({
agentSessionId: fixture.workspaceIsolation.agentSessionId,
projectId: fixture.workspaceIsolation.projectId,
goal: fixture.goal,
prompt: fixture.prompt,
skillCommitId: fixture.skillCommitId,
now: fixedNow
});
expectWorkspaceIsolation(plan, isolationPlan, fixture);
console.log(`[m4-smoke] ok ${fixture.scenario}`);