@aspectlylabs/mikka 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/README.md +79 -2
- package/agents/native-container.mjs +191 -0
- package/agents/native-egress.mjs +51 -0
- package/cli.js +15493 -0
- package/client.js +3789 -0
- package/computer/driver.mjs +278 -0
- package/computer/proxy.mjs +123 -0
- package/coordinator.js +317 -0
- package/env.js +53 -0
- package/fixtures/ask.jsonl +5 -0
- package/fixtures/blocked-question.jsonl +2 -0
- package/fixtures/check-pass.jsonl +3 -0
- package/fixtures/crash.jsonl +6 -0
- package/fixtures/failure.jsonl +8 -0
- package/fixtures/long-running.jsonl +67 -0
- package/fixtures/plan.jsonl +10 -0
- package/fixtures/quota.jsonl +6 -0
- package/fixtures/run-tests.jsonl +8 -0
- package/fixtures/secret-output.jsonl +11 -0
- package/fixtures/self-verify.jsonl +5 -0
- package/fixtures/stall.jsonl +7 -0
- package/fixtures/success-edit.jsonl +19 -0
- package/fixtures/turn-ended.jsonl +18 -0
- package/fixtures/verify-fails.jsonl +7 -0
- package/http-link.js +4502 -0
- package/log.js +62 -0
- package/login.js +3889 -0
- package/main.js +14412 -0
- package/mikka-mcp.mjs +414 -0
- package/mikka.js +15493 -0
- package/package.json +47 -4
- package/paths.js +109 -0
- package/sandbox.js +133 -0
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mikka daemon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The daemon connects your computer to Mikka and runs threads with your installed
|
|
4
|
+
Claude Code or Codex. Node.js 24 or newer and Git are required. Sign in to the
|
|
5
|
+
coding CLI and make sure your local Git credentials can read the repositories
|
|
6
|
+
you want to use.
|
|
7
|
+
|
|
8
|
+
The release candidate is a single `@aspectlylabs/mikka` package containing the
|
|
9
|
+
`mikka` command and its daemon subcommands. The source workspace stays private;
|
|
10
|
+
`pnpm --filter @mikka/daemon build` produces the package with the private
|
|
11
|
+
protocol bundled. The npm organization and trusted publisher must be verified
|
|
12
|
+
before publication.
|
|
13
|
+
|
|
14
|
+
Once the package has been published, install it once and start the daemon:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g @aspectlylabs/mikka
|
|
18
|
+
mikka daemon login
|
|
19
|
+
mikka daemon
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Login prints a browser confirmation link and a short code. The daemon
|
|
23
|
+
saves a device credential, never the browser session. The default server is
|
|
24
|
+
`https://app.aspectlylabs.com/mikka`. For a Preview or self-hosted server, use
|
|
25
|
+
the full application URL (including its base path) in both commands:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
mikka daemon login --server https://preview.example/mikka
|
|
29
|
+
mikka daemon --server https://preview.example/mikka
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`mikka daemon status` reports an already running daemon. `Ctrl+C` shuts it down.
|
|
33
|
+
Manual test setup can use `mikka daemon login --token <device-token>`; keep the
|
|
34
|
+
token out of screenshots, logs and shell history.
|
|
35
|
+
|
|
36
|
+
## Release candidate
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm --filter @mikka/daemon build
|
|
40
|
+
export MIKKA_DAEMON_DIST_DIR=/absolute/path/printed/by/the/build
|
|
41
|
+
pnpm --filter @mikka/daemon test:package
|
|
42
|
+
npm pack "$MIKKA_DAEMON_DIST_DIR"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each build writes a new `.retained-build-*` directory in the source package.
|
|
46
|
+
Existing builds remain intact. The package test requires that exact directory
|
|
47
|
+
and retains its `.retained-package-test-*` installation, tarball and fixture
|
|
48
|
+
index for inspection. The test daemon still performs its normal lifecycle
|
|
49
|
+
cleanup inside the new dedicated fixture; previously retained resources are
|
|
50
|
+
not part of that fixture.
|
|
51
|
+
|
|
52
|
+
No global `tsx` or separate `@mikka/protocol` package is needed. Every push to
|
|
53
|
+
`main` publishes through `.github/workflows/publish-daemon.yml` after daemon
|
|
54
|
+
checks and the installed-package runtime test pass. The workflow uses npm
|
|
55
|
+
trusted publishing and provenance; its npm trust configuration must authorize
|
|
56
|
+
this exact repository and workflow filename.
|
|
57
|
+
|
|
58
|
+
CI stamps `0.0.<workflow run number + 1>` into the source manifest before the
|
|
59
|
+
build, so the npm package and runtime report the same version. Source commits
|
|
60
|
+
keep their development version. Re-running a workflow keeps its version and
|
|
61
|
+
skips publication if npm already records that version from the same commit.
|
|
62
|
+
CI writes the commit SHA into the published manifest as `gitHead`; an existing
|
|
63
|
+
version with absent or different commit metadata stops the release. After
|
|
64
|
+
publication, CI reads npm metadata back and checks the commit SHA. If npm
|
|
65
|
+
serves 404 while the published version becomes available, CI checks up to 31
|
|
66
|
+
times with 10-second waits. Exhausting that wait reports registry verification
|
|
67
|
+
as incomplete; it does not claim that npm rejected publication.
|
|
68
|
+
Releases queue without cancellation (up to GitHub's 100 pending-run limit).
|
|
69
|
+
An older run can publish under its own `build-<version>` tag, preserving the
|
|
70
|
+
newer `latest` version. Manual dispatch remains available on `main` and gets
|
|
71
|
+
a new run version. Running local tests or builds does not publish anything.
|
|
72
|
+
|
|
73
|
+
`test:package` installs the actual tarball into a new directory with public
|
|
74
|
+
registry dependencies, without workspace symlinks or install scripts. Plain
|
|
75
|
+
Node then runs the installed CLI and bundled mock daemon through a real
|
|
76
|
+
clone/worktree/verification/commit/push against a disposable local bare origin.
|
|
77
|
+
It verifies the pushed file and preserves the personal checkout. The test logs
|
|
78
|
+
the tarball digest; its control plane and model are explicit in-memory fixtures.
|
|
79
|
+
This is not real Claude Code/Codex, native-helper, live GitHub App or production
|
|
80
|
+
device-authorization acceptance.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Private hosted-run bootstrap. The first stdin frame is host configuration;
|
|
2
|
+
// later bytes are the existing CLI protocol. No vault/provider token is accepted.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
5
|
+
import { mkdirSync } from "node:fs";
|
|
6
|
+
import { startNativeEgress } from "./native-egress.mjs";
|
|
7
|
+
|
|
8
|
+
const PLACEHOLDER = "mikka-native-broker-placeholder";
|
|
9
|
+
const fail = () => {
|
|
10
|
+
process.stderr.write("native_runtime_unavailable\n");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
};
|
|
13
|
+
let initial = Buffer.alloc(0);
|
|
14
|
+
let starting = false;
|
|
15
|
+
process.stdin.on("data", onData);
|
|
16
|
+
function onData(part) {
|
|
17
|
+
if (starting) return;
|
|
18
|
+
initial = Buffer.concat([initial, part]);
|
|
19
|
+
const newline = initial.indexOf(10);
|
|
20
|
+
if (newline < 0) {
|
|
21
|
+
if (initial.length > 131072) fail();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
starting = true;
|
|
25
|
+
process.stdin.pause();
|
|
26
|
+
process.stdin.removeListener("data", onData);
|
|
27
|
+
let config;
|
|
28
|
+
try {
|
|
29
|
+
config = JSON.parse(initial.subarray(0, newline).toString("utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
fail();
|
|
32
|
+
}
|
|
33
|
+
const remainder = initial.subarray(newline + 1);
|
|
34
|
+
initial = Buffer.alloc(0);
|
|
35
|
+
void start(config, remainder).catch(fail);
|
|
36
|
+
}
|
|
37
|
+
async function start(config, remainder) {
|
|
38
|
+
if (
|
|
39
|
+
!["claude_code", "codex"].includes(config.kind) ||
|
|
40
|
+
!Array.isArray(config.args) ||
|
|
41
|
+
!config.args.every((a) => typeof a === "string") ||
|
|
42
|
+
typeof config.cwd !== "string" ||
|
|
43
|
+
!config.env ||
|
|
44
|
+
typeof config.env !== "object"
|
|
45
|
+
)
|
|
46
|
+
fail();
|
|
47
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
48
|
+
if (
|
|
49
|
+
typeof value !== "string" ||
|
|
50
|
+
/INFISICAL|E2B_API_KEY|REFRESH_TOKEN|^AWS_|^GOOGLE_|^OPENAI_|^ANTHROPIC_.*(?:KEY|TOKEN)|SECRET|PASSWORD/.test(
|
|
51
|
+
key,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
fail();
|
|
55
|
+
if (key === "CLAUDE_CODE_OAUTH_TOKEN" && value !== PLACEHOLDER) fail();
|
|
56
|
+
}
|
|
57
|
+
delete config.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
58
|
+
config.env.PATH = "/usr/local/bin:/usr/bin:/bin";
|
|
59
|
+
mkdirSync(config.env.HOME, { recursive: true, mode: 0o700 });
|
|
60
|
+
// Model credentials stay on the original task broker. Public downloads use
|
|
61
|
+
// a separate live, public-IP-only host proxy, never that credential endpoint.
|
|
62
|
+
const egress = config.publicEgress === true ? await startNativeEgress() : null;
|
|
63
|
+
if (egress) {
|
|
64
|
+
Object.assign(config.env, {
|
|
65
|
+
HTTP_PROXY: egress.url, HTTPS_PROXY: egress.url,
|
|
66
|
+
http_proxy: egress.url, https_proxy: egress.url,
|
|
67
|
+
NO_PROXY: 'localhost,127.0.0.1,::1', no_proxy: 'localhost,127.0.0.1,::1',
|
|
68
|
+
NODE_USE_ENV_PROXY: '1',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// The model transport remains bound to the mounted task broker socket.
|
|
72
|
+
const proxy = createServer((request, response) => {
|
|
73
|
+
if (request.method !== "POST") {
|
|
74
|
+
response.writeHead(404).end();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let path = request.url;
|
|
78
|
+
if (config.kind === "codex" && path.endsWith("/responses"))
|
|
79
|
+
path = "/responses";
|
|
80
|
+
const upstream = httpRequest(
|
|
81
|
+
{
|
|
82
|
+
socketPath: "/run/mikka-native.sock",
|
|
83
|
+
method: "POST",
|
|
84
|
+
path,
|
|
85
|
+
headers: request.headers,
|
|
86
|
+
},
|
|
87
|
+
(result) => {
|
|
88
|
+
response.writeHead(result.statusCode, {
|
|
89
|
+
"content-type": result.headers["content-type"] ?? "application/json",
|
|
90
|
+
});
|
|
91
|
+
result.pipe(response);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
upstream.on("error", () => response.destroy());
|
|
95
|
+
request.pipe(upstream);
|
|
96
|
+
response.on("close", () => upstream.destroy());
|
|
97
|
+
});
|
|
98
|
+
await new Promise((resolve) => proxy.listen(0, "127.0.0.1", resolve));
|
|
99
|
+
const base = `http://127.0.0.1:${proxy.address().port}`;
|
|
100
|
+
const args = [...config.args];
|
|
101
|
+
if (config.kind === "claude_code") {
|
|
102
|
+
config.env.ANTHROPIC_BASE_URL = base;
|
|
103
|
+
config.env.CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR = "3";
|
|
104
|
+
// Docker confines all tools here; never request a privileged nested sandbox.
|
|
105
|
+
const settingsIndex = args.findIndex(
|
|
106
|
+
(value) => value === "--settings" || value.startsWith("--settings="),
|
|
107
|
+
);
|
|
108
|
+
if (settingsIndex < 0) fail();
|
|
109
|
+
const inline = args[settingsIndex].startsWith("--settings=");
|
|
110
|
+
const settings = JSON.parse(
|
|
111
|
+
inline
|
|
112
|
+
? args[settingsIndex].slice("--settings=".length)
|
|
113
|
+
: args[settingsIndex + 1],
|
|
114
|
+
);
|
|
115
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
116
|
+
fail();
|
|
117
|
+
settings.sandbox = { ...settings.sandbox, enabled: false };
|
|
118
|
+
if (inline) args[settingsIndex] = "--settings=" + JSON.stringify(settings);
|
|
119
|
+
else args[settingsIndex + 1] = JSON.stringify(settings);
|
|
120
|
+
for (let i = 0; i < args.length; i++)
|
|
121
|
+
if (args[i] === "--mcp-config" && args[i + 1]) {
|
|
122
|
+
const value = JSON.parse(args[i + 1]);
|
|
123
|
+
if (value.mcpServers?.mikka)
|
|
124
|
+
value.mcpServers.mikka.command = "/usr/local/bin/node";
|
|
125
|
+
args[i + 1] = JSON.stringify(value);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
args.push(
|
|
129
|
+
"-c",
|
|
130
|
+
`chatgpt_base_url=${JSON.stringify(base + "/backend-api/")}`,
|
|
131
|
+
);
|
|
132
|
+
args.push(
|
|
133
|
+
"-c",
|
|
134
|
+
"model_providers.openai.supports_websockets=false",
|
|
135
|
+
"-c",
|
|
136
|
+
"features.responses_websockets=false",
|
|
137
|
+
"-c",
|
|
138
|
+
"features.responses_websockets_v2=false",
|
|
139
|
+
);
|
|
140
|
+
args.push("-c", 'mcp_servers.mikka.command="/usr/local/bin/node"');
|
|
141
|
+
for (const key of [
|
|
142
|
+
"GIT_DIR",
|
|
143
|
+
"GIT_WORK_TREE",
|
|
144
|
+
"GIT_CONFIG_GLOBAL",
|
|
145
|
+
"GIT_CONFIG_SYSTEM",
|
|
146
|
+
"GIT_TERMINAL_PROMPT",
|
|
147
|
+
"HOME",
|
|
148
|
+
...(egress ? ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy", "NODE_USE_ENV_PROXY"] : []),
|
|
149
|
+
])
|
|
150
|
+
args.push(
|
|
151
|
+
"-c",
|
|
152
|
+
`shell_environment_policy.set.${key}=${JSON.stringify(config.env[key])}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const child = spawn("/usr/local/bin/native-cli", args, {
|
|
156
|
+
cwd: config.cwd,
|
|
157
|
+
env: config.env,
|
|
158
|
+
stdio:
|
|
159
|
+
config.kind === "claude_code"
|
|
160
|
+
? ["pipe", "pipe", "pipe", "pipe"]
|
|
161
|
+
: ["pipe", "pipe", "pipe"],
|
|
162
|
+
});
|
|
163
|
+
if (config.kind === "claude_code") {
|
|
164
|
+
child.stdio[3].on("error", () => child.kill("SIGTERM"));
|
|
165
|
+
child.stdio[3].end(PLACEHOLDER);
|
|
166
|
+
}
|
|
167
|
+
// Raw native diagnostics can echo model data: never publish stderr.
|
|
168
|
+
child.stderr.resume();
|
|
169
|
+
child.stdout.pipe(process.stdout);
|
|
170
|
+
if (remainder.length) child.stdin.write(remainder);
|
|
171
|
+
process.stdin.pipe(child.stdin);
|
|
172
|
+
process.stdin.resume();
|
|
173
|
+
child.stdin.on("error", () => undefined);
|
|
174
|
+
let timer;
|
|
175
|
+
const stop = () => {
|
|
176
|
+
child.kill("SIGTERM");
|
|
177
|
+
timer ??= setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
178
|
+
};
|
|
179
|
+
process.on("SIGTERM", stop);
|
|
180
|
+
process.on("SIGINT", stop);
|
|
181
|
+
process.stdin.on("end", () => {
|
|
182
|
+
child.stdin.end();
|
|
183
|
+
timer ??= setTimeout(stop, 3000);
|
|
184
|
+
});
|
|
185
|
+
child.on("error", fail);
|
|
186
|
+
child.on("exit", (code) => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
proxy.closeAllConnections();
|
|
189
|
+
void (egress?.close() ?? Promise.resolve()).finally(() => proxy.close(() => process.exit(code ?? 1)));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Only the host-supplied configuration enables this relay. The container has
|
|
2
|
+
// no network interface; it forwards bytes to its single mounted egress socket.
|
|
3
|
+
import { createServer, request } from 'node:http'
|
|
4
|
+
|
|
5
|
+
export async function startNativeEgress(socketPath = '/run/mikka-egress.sock') {
|
|
6
|
+
const sockets = new Set()
|
|
7
|
+
const track = socket => {
|
|
8
|
+
sockets.add(socket)
|
|
9
|
+
socket.on('error', () => {})
|
|
10
|
+
socket.once('close', () => sockets.delete(socket))
|
|
11
|
+
return socket
|
|
12
|
+
}
|
|
13
|
+
const server = createServer({ maxHeaderSize: 16384 }, (incoming, response) => {
|
|
14
|
+
const outgoing = request({ socketPath, path: incoming.url, method: incoming.method, headers: incoming.headers, agent: false }, upstream => {
|
|
15
|
+
response.writeHead(upstream.statusCode ?? 502, upstream.headers)
|
|
16
|
+
upstream.pipe(response)
|
|
17
|
+
})
|
|
18
|
+
outgoing.on('socket', track)
|
|
19
|
+
outgoing.on('error', () => { if (!response.headersSent) response.writeHead(502); response.end() })
|
|
20
|
+
incoming.once('aborted', () => outgoing.destroy())
|
|
21
|
+
response.once('close', () => outgoing.destroy())
|
|
22
|
+
incoming.pipe(outgoing)
|
|
23
|
+
})
|
|
24
|
+
server.on('connection', track)
|
|
25
|
+
server.on('connect', (incoming, client, head) => {
|
|
26
|
+
const outgoing = request({ socketPath, method: 'CONNECT', path: incoming.url, headers: { host: incoming.url }, agent: false })
|
|
27
|
+
outgoing.on('socket', track)
|
|
28
|
+
outgoing.on('connect', (response, upstream, upstreamHead) => {
|
|
29
|
+
if (response.statusCode !== 200) { client.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n'); upstream.destroy(); return }
|
|
30
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
31
|
+
if (upstreamHead.length) client.write(upstreamHead)
|
|
32
|
+
if (head.length) upstream.write(head)
|
|
33
|
+
upstream.pipe(client);client.pipe(upstream)
|
|
34
|
+
client.once('close', () => upstream.destroy())
|
|
35
|
+
upstream.once('close', () => client.destroy())
|
|
36
|
+
})
|
|
37
|
+
outgoing.on('error', () => client.destroy())
|
|
38
|
+
client.once('close', () => outgoing.destroy())
|
|
39
|
+
outgoing.end()
|
|
40
|
+
})
|
|
41
|
+
server.on('upgrade', (_request, socket) => socket.destroy())
|
|
42
|
+
server.on('clientError', (_error, socket) => socket.destroy())
|
|
43
|
+
await new Promise((resolve, reject) => { server.once('error', reject);server.listen(0, '127.0.0.1', resolve) })
|
|
44
|
+
return {
|
|
45
|
+
url: `http://127.0.0.1:${server.address().port}`,
|
|
46
|
+
async close() {
|
|
47
|
+
for (const socket of sockets) socket.destroy()
|
|
48
|
+
await new Promise(resolve => server.close(resolve))
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|