@tarfa/cli 0.1.3 → 0.1.5
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/dist/{chunk-X6FKPFHV.js → chunk-KTUXNG4Q.js} +111 -64
- package/dist/cli.js +43 -65
- package/dist/serve.js +30 -43
- package/package.json +1 -1
- package/templates/tarfa-agent.sh +11 -2
- package/templates/tarfa-cli-wrapper.sh +11 -2
|
@@ -3546,9 +3546,9 @@ function constantTimeEqual(a, b) {
|
|
|
3546
3546
|
var REFUSAL = {
|
|
3547
3547
|
notEnrolled: "device not enrolled",
|
|
3548
3548
|
revoked: "device revoked",
|
|
3549
|
-
//
|
|
3549
|
+
// Sent only by agents from before token enrolment was removed: a device whose key is enrolled
|
|
3550
|
+
// sent the token anyway, and token enrolment off (only a pairing code enrols a new phone).
|
|
3550
3551
|
tokenNotNeeded: "token not needed",
|
|
3551
|
-
// Token enrolment is off: only a pairing code enrols a new phone.
|
|
3552
3552
|
enrolmentOff: "enrolment off",
|
|
3553
3553
|
// No new phone can enrol, pairing code or not.
|
|
3554
3554
|
enrolmentClosed: "enrolment closed",
|
|
@@ -3782,12 +3782,6 @@ function planSetup(flags, state) {
|
|
|
3782
3782
|
}
|
|
3783
3783
|
const addresses = [...derived ? [derived] : [], ...flags.addresses];
|
|
3784
3784
|
if (addresses.length > MAX_ADDRESSES) return { error: `a machine takes at most ${MAX_ADDRESSES} addresses (got ${addresses.length})` };
|
|
3785
|
-
const published = addresses.length > 0 || Boolean(withLegacyEnv(parseEnvFile(state.envText ?? "")).TARFA_PHONE_ADDRESSES);
|
|
3786
|
-
if (published && state.roster === "on" && flags.tokenEnrolment === void 0) {
|
|
3787
|
-
return {
|
|
3788
|
-
error: "token enrolment is on in ~/.tarfa/devices.json, and this run gives phones an address: pass --token-enrolment off to switch it off (pairing codes only), or --token-enrolment on to keep it on deliberately"
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
3785
|
if (state.legacyService) steps.push({ kind: "stop-legacy-service" });
|
|
3792
3786
|
if (state.stateDir === "legacy") steps.push({ kind: "move-state" });
|
|
3793
3787
|
const adoption = state.adoption;
|
|
@@ -3796,12 +3790,7 @@ function planSetup(flags, state) {
|
|
|
3796
3790
|
const env = planEnv(flags, state, addresses);
|
|
3797
3791
|
if (env) steps.push(env);
|
|
3798
3792
|
if (!state.hasKey) steps.push({ kind: "create-key" });
|
|
3799
|
-
if (state.roster === "absent") steps.push({ kind: "write-roster"
|
|
3800
|
-
else if (flags.tokenEnrolment && state.roster !== flags.tokenEnrolment) steps.push({ kind: "write-roster", mode: flags.tokenEnrolment });
|
|
3801
|
-
const mode = state.roster === "absent" ? flags.tokenEnrolment ?? "off" : flags.tokenEnrolment ?? state.roster;
|
|
3802
|
-
if (mode === "on" && checks.some((line) => line.includes("internet") || line.includes("Funnel allowed"))) {
|
|
3803
|
-
checks.push("warning: token enrolment is on, so whoever holds TARFA_TOKEN can enrol a phone on a public port: run `tarfa devices enrolment off` (or pass --token-enrolment off)");
|
|
3804
|
-
}
|
|
3793
|
+
if (state.roster === "absent") steps.push({ kind: "write-roster" });
|
|
3805
3794
|
const { kind: unitKind, installed } = state.service;
|
|
3806
3795
|
const adopted = unitRunsLauncher(unitKind, installed, state.launcher.path);
|
|
3807
3796
|
const serviceTarget = state.install === "package" || adopted ? { kind: "launcher", path: state.launcher.path } : { kind: "clone", repo: state.repo ?? "" };
|
|
@@ -3836,7 +3825,6 @@ function planEnv(flags, state, addresses) {
|
|
|
3836
3825
|
const before = legacy.text;
|
|
3837
3826
|
const values = parseEnvFile(before);
|
|
3838
3827
|
const wanted = [];
|
|
3839
|
-
if (!values.TARFA_TOKEN) wanted.push(["TARFA_TOKEN", state.newToken, "(generated)"]);
|
|
3840
3828
|
wanted.push(["TARFA_MACHINE_ID", flags.machineId], ["TARFA_PROJECT_ROOTS", flags.projectRoots.join(":")]);
|
|
3841
3829
|
if (flags.defaultPath) wanted.push(["TARFA_DEFAULT_PATH", flags.defaultPath]);
|
|
3842
3830
|
wanted.push(["TARFA_PHONE_PORT", String(flags.phonePort)]);
|
|
@@ -3926,7 +3914,7 @@ function describeStep(step, envPath) {
|
|
|
3926
3914
|
case "create-key":
|
|
3927
3915
|
return "create the agent key (~/.tarfa/agent-key.json)";
|
|
3928
3916
|
case "write-roster":
|
|
3929
|
-
return
|
|
3917
|
+
return "write ~/.tarfa/devices.json (phones join with a `tarfa pair` code)";
|
|
3930
3918
|
case "write-launcher":
|
|
3931
3919
|
return `write the launcher the service runs (${step.path})`;
|
|
3932
3920
|
case "install-unit":
|
|
@@ -3945,7 +3933,7 @@ function describeStep(step, envPath) {
|
|
|
3945
3933
|
return `ask the node's Tailscale operator to run: ${step.line}`;
|
|
3946
3934
|
}
|
|
3947
3935
|
}
|
|
3948
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--machine-id", "--project-roots", "--default-path", "--phone-port", "--slug", "--tailscale", "--tailscale-port", "--address", "--pwa"
|
|
3936
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--machine-id", "--project-roots", "--default-path", "--phone-port", "--slug", "--tailscale", "--tailscale-port", "--address", "--pwa"]);
|
|
3949
3937
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--pair", "--dry-run", "--adopt"]);
|
|
3950
3938
|
function portNumber(text) {
|
|
3951
3939
|
if (!/^\d{1,5}$/.test(text)) return null;
|
|
@@ -3999,8 +3987,6 @@ function parseSetupFlags(argv) {
|
|
|
3999
3987
|
return { error: `--pwa must be the http(s) origin phones open the app at (got ${single.get("--pwa")})` };
|
|
4000
3988
|
}
|
|
4001
3989
|
}
|
|
4002
|
-
const tokenEnrolment = single.get("--token-enrolment");
|
|
4003
|
-
if (tokenEnrolment !== void 0 && tokenEnrolment !== "on" && tokenEnrolment !== "off") return { error: "--token-enrolment takes on or off" };
|
|
4004
3990
|
return {
|
|
4005
3991
|
machineId,
|
|
4006
3992
|
projectRoots,
|
|
@@ -4013,8 +3999,7 @@ function parseSetupFlags(argv) {
|
|
|
4013
3999
|
...pwa !== void 0 ? { pwa } : {},
|
|
4014
4000
|
pair: set.has("--pair"),
|
|
4015
4001
|
dryRun: set.has("--dry-run"),
|
|
4016
|
-
adopt: set.has("--adopt")
|
|
4017
|
-
...tokenEnrolment !== void 0 ? { tokenEnrolment } : {}
|
|
4002
|
+
adopt: set.has("--adopt")
|
|
4018
4003
|
};
|
|
4019
4004
|
}
|
|
4020
4005
|
|
|
@@ -4211,7 +4196,6 @@ function loadConfig(rawEnv, home) {
|
|
|
4211
4196
|
return {
|
|
4212
4197
|
stateDir,
|
|
4213
4198
|
machineId,
|
|
4214
|
-
token: env.TARFA_TOKEN,
|
|
4215
4199
|
port,
|
|
4216
4200
|
phonePort,
|
|
4217
4201
|
phoneAddresses: phoneAddresses(env),
|
|
@@ -4244,11 +4228,6 @@ function loadConfig(rawEnv, home) {
|
|
|
4244
4228
|
...env.TARFA_VOICE_DEVICE?.trim() ? { voiceDevice: env.TARFA_VOICE_DEVICE.trim() } : {}
|
|
4245
4229
|
};
|
|
4246
4230
|
}
|
|
4247
|
-
function assertServerConfig(config) {
|
|
4248
|
-
if (!config.token) {
|
|
4249
|
-
throw new Error("TARFA_TOKEN is required to run the agent server");
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
4231
|
function configWarnings(rawEnv) {
|
|
4253
4232
|
const env = withLegacyEnv(rawEnv);
|
|
4254
4233
|
const warnings = legacyEnvNames(rawEnv).map(
|
|
@@ -4256,6 +4235,7 @@ function configWarnings(rawEnv) {
|
|
|
4256
4235
|
);
|
|
4257
4236
|
const relays = ["TARFA_RELAY_URLS", "TARFA_RELAY_URL"].filter((name) => env[name]?.trim());
|
|
4258
4237
|
if (relays.length > 0) warnings.push(`relays are gone; remove ${relays.join(" and ")} from agent.env`);
|
|
4238
|
+
if (env.TARFA_TOKEN?.trim()) warnings.push("TARFA_TOKEN is ignored: phones join only with a `tarfa pair` link; remove it from agent.env");
|
|
4259
4239
|
if (phoneAddresses(env).length === 0) {
|
|
4260
4240
|
warnings.push("no TARFA_PHONE_ADDRESSES: phones reach this agent only once its phone port is published (tailscale serve, a reverse proxy)");
|
|
4261
4241
|
}
|
|
@@ -4452,8 +4432,9 @@ var TmuxAdapter = class {
|
|
|
4452
4432
|
return set;
|
|
4453
4433
|
}
|
|
4454
4434
|
async createSession(input) {
|
|
4435
|
+
const env = Object.entries(input.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
4455
4436
|
try {
|
|
4456
|
-
await this.tmux(["new-session", "-d", "-s", input.name, "-c", input.cwd, input.command]);
|
|
4437
|
+
await this.tmux(["new-session", "-d", "-s", input.name, "-c", input.cwd, ...env, input.command]);
|
|
4457
4438
|
} catch (err) {
|
|
4458
4439
|
if ((err.stderr ?? "").includes("duplicate session")) throw new DuplicateSessionError(input.name);
|
|
4459
4440
|
throw err;
|
|
@@ -4989,17 +4970,12 @@ function parseKeys(text, path) {
|
|
|
4989
4970
|
}
|
|
4990
4971
|
|
|
4991
4972
|
// src/application/device-roster.ts
|
|
4992
|
-
var EMPTY_ROSTER = { enrolment: true, devices: [], revoked: [] };
|
|
4973
|
+
var EMPTY_ROSTER = { enrolment: true, tokenEnrolment: false, devices: [], revoked: [] };
|
|
4993
4974
|
function enrolmentMode(roster) {
|
|
4994
|
-
|
|
4995
|
-
return roster.tokenEnrolment === false ? "off" : "on";
|
|
4975
|
+
return roster.enrolment ? "open" : "closed";
|
|
4996
4976
|
}
|
|
4997
4977
|
function withEnrolmentMode(roster, mode) {
|
|
4998
|
-
return { ...roster, enrolment: mode
|
|
4999
|
-
}
|
|
5000
|
-
function mayEnrol(roster, via) {
|
|
5001
|
-
const mode = enrolmentMode(roster);
|
|
5002
|
-
return mode === "on" || mode === "off" && via === "pairing";
|
|
4978
|
+
return { ...roster, enrolment: mode === "open", tokenEnrolment: false };
|
|
5003
4979
|
}
|
|
5004
4980
|
function standingOf(roster, x255192) {
|
|
5005
4981
|
if (roster.revoked.some((d) => d.x25519 === x255192)) return "revoked";
|
|
@@ -5045,7 +5021,8 @@ function forgetDevice(roster, id2) {
|
|
|
5045
5021
|
}
|
|
5046
5022
|
|
|
5047
5023
|
// src/infrastructure/file-device-store.ts
|
|
5048
|
-
import { readFile as readFile5 } from "node:fs/promises";
|
|
5024
|
+
import { mkdir as mkdir5, readFile as readFile5 } from "node:fs/promises";
|
|
5025
|
+
import { dirname as dirname6 } from "node:path";
|
|
5049
5026
|
import { z as z2 } from "zod";
|
|
5050
5027
|
var key = z2.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
5051
5028
|
var device = z2.object({ id: z2.string(), x25519: key, ed25519: key, name: z2.string(), enrolledAt: z2.string() });
|
|
@@ -5074,9 +5051,31 @@ var FileDeviceStore = class {
|
|
|
5074
5051
|
if (!parsed.success) throw new Error(`device roster ${this.filePath} is malformed; fix or move it aside`);
|
|
5075
5052
|
return parsed.data;
|
|
5076
5053
|
}
|
|
5054
|
+
// Writes the roster once if the file lacks `tokenEnrolment: false`, missing file included, so an
|
|
5055
|
+
// agent from before token enrolment was removed, brought back, finds it off. True if it wrote.
|
|
5056
|
+
async sealTokenEnrolment() {
|
|
5057
|
+
let raw = null;
|
|
5058
|
+
try {
|
|
5059
|
+
raw = await readFile5(this.filePath, "utf8");
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
if (err.code !== "ENOENT") throw err;
|
|
5062
|
+
}
|
|
5063
|
+
if (raw !== null) {
|
|
5064
|
+
let parsed = null;
|
|
5065
|
+
try {
|
|
5066
|
+
parsed = JSON.parse(raw);
|
|
5067
|
+
} catch {
|
|
5068
|
+
return false;
|
|
5069
|
+
}
|
|
5070
|
+
if (parsed?.tokenEnrolment === false) return false;
|
|
5071
|
+
}
|
|
5072
|
+
await mkdir5(dirname6(this.filePath), { recursive: true, mode: 448 });
|
|
5073
|
+
await this.update((roster) => roster);
|
|
5074
|
+
return true;
|
|
5075
|
+
}
|
|
5077
5076
|
async update(change) {
|
|
5078
5077
|
return withFileLock(this.filePath, this.staleLockMs, async () => {
|
|
5079
|
-
const next = change(await this.read());
|
|
5078
|
+
const next = { ...change(await this.read()), tokenEnrolment: false };
|
|
5080
5079
|
await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
|
|
5081
5080
|
`);
|
|
5082
5081
|
return next;
|
|
@@ -5140,8 +5139,8 @@ var FilePairingStore = class {
|
|
|
5140
5139
|
// src/infrastructure/agent-env-file.ts
|
|
5141
5140
|
import { execFile as execFile3 } from "node:child_process";
|
|
5142
5141
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5143
|
-
import { chmod, copyFile, mkdir as
|
|
5144
|
-
import { dirname as
|
|
5142
|
+
import { chmod, copyFile, mkdir as mkdir6, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
|
|
5143
|
+
import { dirname as dirname7, resolve as resolve5 } from "node:path";
|
|
5145
5144
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5146
5145
|
import { promisify as promisify3 } from "node:util";
|
|
5147
5146
|
|
|
@@ -5161,10 +5160,10 @@ function withoutGitVars(env) {
|
|
|
5161
5160
|
}
|
|
5162
5161
|
async function mainCheckout(kind2 = installKind()) {
|
|
5163
5162
|
if (kind2 === "package") return null;
|
|
5164
|
-
const here =
|
|
5163
|
+
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
5165
5164
|
try {
|
|
5166
5165
|
const { stdout } = await exec3("git", ["-C", here, "rev-parse", "--path-format=absolute", "--git-common-dir"], { env: withoutGitVars(process.env) });
|
|
5167
|
-
return
|
|
5166
|
+
return dirname7(stdout.trim());
|
|
5168
5167
|
} catch {
|
|
5169
5168
|
return resolve5(here, "..", "..", "..", "..");
|
|
5170
5169
|
}
|
|
@@ -5194,7 +5193,7 @@ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
|
5194
5193
|
if (err.code !== "ENOENT") throw err;
|
|
5195
5194
|
backup = null;
|
|
5196
5195
|
}
|
|
5197
|
-
await
|
|
5196
|
+
await mkdir6(dirname7(path), { recursive: true, mode: 448 });
|
|
5198
5197
|
await writeFile4(path, text, { mode: 384 });
|
|
5199
5198
|
await chmod(path, 384);
|
|
5200
5199
|
return backup;
|
|
@@ -5204,7 +5203,7 @@ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
|
5204
5203
|
|
|
5205
5204
|
// src/infrastructure/file-status-store.ts
|
|
5206
5205
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
5207
|
-
import { dirname as
|
|
5206
|
+
import { dirname as dirname8 } from "node:path";
|
|
5208
5207
|
|
|
5209
5208
|
// ../contracts/src/domain/workspace-status.ts
|
|
5210
5209
|
var WORKSPACE_STATUSES = [
|
|
@@ -5390,6 +5389,7 @@ var machineStatsSchema = z6.object({
|
|
|
5390
5389
|
}).optional()
|
|
5391
5390
|
});
|
|
5392
5391
|
var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
5392
|
+
// `token` carries a `tarfa pair` code for a new phone, or nothing (an enrolled key needs none).
|
|
5393
5393
|
z6.object({ type: z6.literal("auth"), token: z6.string() }),
|
|
5394
5394
|
z6.object({ type: z6.literal("list") }),
|
|
5395
5395
|
z6.object({ type: z6.literal("getRecentPaths") }),
|
|
@@ -5442,8 +5442,9 @@ var detachReasonSchema = z6.enum(["opened-elsewhere", "closed", "error"]);
|
|
|
5442
5442
|
var PROTOCOL_VERSION = 1;
|
|
5443
5443
|
var AGENT_FEATURES = { lastReply: "lastReply" };
|
|
5444
5444
|
var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
5445
|
-
// `credential` is what authenticated the session
|
|
5446
|
-
//
|
|
5445
|
+
// `credential` is what authenticated the session (`token` only from an agent from before token
|
|
5446
|
+
// enrolment was removed); `enrolled` says the device key is enrolled (written), so the phone
|
|
5447
|
+
// forgets its pairing secret. Both optional: an older agent sends neither.
|
|
5447
5448
|
// `pushed`: the agent served the phone at once (its hello asked) and has sent the opening replies
|
|
5448
5449
|
// (workspaces, keyterms) behind it, so the phone does not ask for them.
|
|
5449
5450
|
z6.object({
|
|
@@ -5495,7 +5496,7 @@ var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5495
5496
|
z6.object({ type: z6.literal("voiceDone"), id: z6.string(), ok: z6.boolean(), error: z6.string().optional() }),
|
|
5496
5497
|
z6.object({ type: z6.literal("detached"), workspaceId: z6.string(), reason: detachReasonSchema }),
|
|
5497
5498
|
z6.object({ type: z6.literal("closed"), workspaceId: z6.string() }),
|
|
5498
|
-
// A phone was just enrolled on this machine (a pairing code
|
|
5499
|
+
// A phone was just enrolled on this machine (a pairing code), told to every other
|
|
5499
5500
|
// phone connected to it: a device nobody meant to add must not go unnoticed. An older PWA drops
|
|
5500
5501
|
// the unknown type.
|
|
5501
5502
|
z6.object({ type: z6.literal("deviceEnrolled"), id: z6.string(), name: z6.string(), at: z6.string() }),
|
|
@@ -5696,7 +5697,7 @@ var FileStatusStore = class {
|
|
|
5696
5697
|
}
|
|
5697
5698
|
}
|
|
5698
5699
|
persist() {
|
|
5699
|
-
mkdirSync(
|
|
5700
|
+
mkdirSync(dirname8(this.filePath), { recursive: true });
|
|
5700
5701
|
writeFileSync(this.filePath, JSON.stringify(Object.fromEntries(this.statuses), null, 2), "utf8");
|
|
5701
5702
|
}
|
|
5702
5703
|
};
|
|
@@ -5725,8 +5726,8 @@ function parseEntry(value) {
|
|
|
5725
5726
|
}
|
|
5726
5727
|
|
|
5727
5728
|
// src/infrastructure/file-parked-store.ts
|
|
5728
|
-
import { readFile as readFile8, writeFile as writeFile5, mkdir as
|
|
5729
|
-
import { dirname as
|
|
5729
|
+
import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir7 } from "node:fs/promises";
|
|
5730
|
+
import { dirname as dirname9 } from "node:path";
|
|
5730
5731
|
var FileParkedStore = class {
|
|
5731
5732
|
constructor(filePath) {
|
|
5732
5733
|
this.filePath = filePath;
|
|
@@ -5760,7 +5761,7 @@ var FileParkedStore = class {
|
|
|
5760
5761
|
await this.persist(rest);
|
|
5761
5762
|
}
|
|
5762
5763
|
async persist(entries) {
|
|
5763
|
-
await
|
|
5764
|
+
await mkdir7(dirname9(this.filePath), { recursive: true });
|
|
5764
5765
|
await writeFile5(this.filePath, JSON.stringify(entries, null, 2), "utf8");
|
|
5765
5766
|
}
|
|
5766
5767
|
};
|
|
@@ -5795,10 +5796,10 @@ function parseEntry2(value) {
|
|
|
5795
5796
|
}
|
|
5796
5797
|
|
|
5797
5798
|
// src/infrastructure/fs-file-writer.ts
|
|
5798
|
-
import { mkdir as
|
|
5799
|
+
import { mkdir as mkdir8, writeFile as writeFile6, access } from "node:fs/promises";
|
|
5799
5800
|
var FsFileWriter = class {
|
|
5800
5801
|
async ensureDir(dir) {
|
|
5801
|
-
await
|
|
5802
|
+
await mkdir8(dir, { recursive: true });
|
|
5802
5803
|
}
|
|
5803
5804
|
async exists(path) {
|
|
5804
5805
|
try {
|
|
@@ -5878,12 +5879,12 @@ function loginShellResolver(shell = process.env.SHELL || "/bin/sh") {
|
|
|
5878
5879
|
}
|
|
5879
5880
|
|
|
5880
5881
|
// src/infrastructure/codex-app-server.ts
|
|
5881
|
-
import {
|
|
5882
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
5882
5883
|
import { join as join7 } from "node:path";
|
|
5883
5884
|
import { WebSocket } from "ws";
|
|
5884
5885
|
var CodexAppServerDown = class extends Error {
|
|
5885
|
-
constructor() {
|
|
5886
|
-
super(
|
|
5886
|
+
constructor(socketPath) {
|
|
5887
|
+
super(`the Codex app server is not running (no socket at ${socketPath})`);
|
|
5887
5888
|
}
|
|
5888
5889
|
};
|
|
5889
5890
|
var CodexRpcError = class extends Error {
|
|
@@ -5954,8 +5955,8 @@ var Connection = class {
|
|
|
5954
5955
|
var CodexAppServer = class {
|
|
5955
5956
|
socketPath;
|
|
5956
5957
|
timeoutMs;
|
|
5957
|
-
constructor(options
|
|
5958
|
-
this.socketPath = options.socketPath
|
|
5958
|
+
constructor(options) {
|
|
5959
|
+
this.socketPath = "socketPath" in options ? options.socketPath : join7(options.codexHome, "app-server-control", "app-server-control.sock");
|
|
5959
5960
|
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
5960
5961
|
}
|
|
5961
5962
|
loadedThreads() {
|
|
@@ -6009,9 +6010,15 @@ var CodexAppServer = class {
|
|
|
6009
6010
|
return thread.id;
|
|
6010
6011
|
});
|
|
6011
6012
|
}
|
|
6012
|
-
|
|
6013
|
+
// Codex 0.167 binds the socket under /tmp/codex-daemon-<uid>/ and publishes this path as a link
|
|
6014
|
+
// to it. Connecting to the link's target keeps the path within the 107 bytes a unix socket
|
|
6015
|
+
// address holds, however deep CODEX_HOME is; a link to nothing means no daemon.
|
|
6016
|
+
async call(run) {
|
|
6017
|
+
const target = await realpath3(this.socketPath).catch((err) => {
|
|
6018
|
+
throw err.code === "ENOENT" ? new CodexAppServerDown(this.socketPath) : err;
|
|
6019
|
+
});
|
|
6013
6020
|
return new Promise((resolve6, reject) => {
|
|
6014
|
-
const ws = new WebSocket(`ws+unix://${
|
|
6021
|
+
const ws = new WebSocket(`ws+unix://${target}:/`, { perMessageDeflate: false });
|
|
6015
6022
|
const c = new Connection(ws);
|
|
6016
6023
|
let settled = false;
|
|
6017
6024
|
const settle = (fn) => {
|
|
@@ -6030,7 +6037,7 @@ var CodexAppServer = class {
|
|
|
6030
6037
|
}, this.timeoutMs);
|
|
6031
6038
|
ws.once("error", (err) => {
|
|
6032
6039
|
const down = err.code === "ENOENT" || err.code === "ECONNREFUSED";
|
|
6033
|
-
settle(() => reject(down ? new CodexAppServerDown() : err));
|
|
6040
|
+
settle(() => reject(down ? new CodexAppServerDown(this.socketPath) : err));
|
|
6034
6041
|
});
|
|
6035
6042
|
ws.once("close", () => settle(() => reject(new Error("the Codex app server closed the connection"))));
|
|
6036
6043
|
ws.once("open", () => {
|
|
@@ -6046,6 +6053,45 @@ var CodexAppServer = class {
|
|
|
6046
6053
|
}
|
|
6047
6054
|
};
|
|
6048
6055
|
|
|
6056
|
+
// src/infrastructure/codex-versions.ts
|
|
6057
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
6058
|
+
function loginShell(shell = process.env.SHELL || "/bin/sh", extraEnv = {}) {
|
|
6059
|
+
return (script) => new Promise((resolve6, reject) => {
|
|
6060
|
+
execFile5(shell, ["-lc", script], { env: { ...childEnv(), ...extraEnv }, timeout: 15e3 }, (err, stdout) => err ? reject(err) : resolve6(stdout));
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
function codexVersions(run = loginShell()) {
|
|
6064
|
+
return async () => {
|
|
6065
|
+
const [cliOut, daemonOut] = await Promise.all([
|
|
6066
|
+
run("codex --version").catch((err) => {
|
|
6067
|
+
throw new Error(`cannot run codex --version: ${err.message}`);
|
|
6068
|
+
}),
|
|
6069
|
+
run("codex app-server daemon version").catch((err) => {
|
|
6070
|
+
throw new Error(`cannot read the Codex app server's version: ${err.message}`);
|
|
6071
|
+
})
|
|
6072
|
+
]);
|
|
6073
|
+
const cli = cliOut.trim().split(/\s+/).pop() ?? "";
|
|
6074
|
+
let appServer;
|
|
6075
|
+
try {
|
|
6076
|
+
appServer = JSON.parse(daemonOut).appServerVersion;
|
|
6077
|
+
} catch {
|
|
6078
|
+
appServer = void 0;
|
|
6079
|
+
}
|
|
6080
|
+
if (typeof appServer !== "string" || appServer === "") throw new Error("the Codex app server is not running");
|
|
6081
|
+
return { cli, appServer };
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
|
|
6085
|
+
// src/infrastructure/codex-home.ts
|
|
6086
|
+
import { join as join8 } from "node:path";
|
|
6087
|
+
var MARK = "codex-home=";
|
|
6088
|
+
async function resolveCodexHome(env, home, run = loginShell()) {
|
|
6089
|
+
if (env.CODEX_HOME) return env.CODEX_HOME;
|
|
6090
|
+
const out = await run(`printf '\\n${MARK}%s\\n' "\${CODEX_HOME-}"`).catch(() => "");
|
|
6091
|
+
const value = out.split("\n").reverse().find((line) => line.startsWith(MARK))?.slice(MARK.length);
|
|
6092
|
+
return value || join8(home, ".codex");
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6049
6095
|
export {
|
|
6050
6096
|
packageRoot,
|
|
6051
6097
|
packageVersion,
|
|
@@ -6090,7 +6136,6 @@ export {
|
|
|
6090
6136
|
stateDirLayout,
|
|
6091
6137
|
migrateStateDir,
|
|
6092
6138
|
loadConfig,
|
|
6093
|
-
assertServerConfig,
|
|
6094
6139
|
configWarnings,
|
|
6095
6140
|
systemClock,
|
|
6096
6141
|
systemPathResolver,
|
|
@@ -6116,7 +6161,6 @@ export {
|
|
|
6116
6161
|
loadOrCreateAgentKeys,
|
|
6117
6162
|
enrolmentMode,
|
|
6118
6163
|
withEnrolmentMode,
|
|
6119
|
-
mayEnrol,
|
|
6120
6164
|
standingOf,
|
|
6121
6165
|
enrolDevice,
|
|
6122
6166
|
revokeDevice,
|
|
@@ -6151,5 +6195,8 @@ export {
|
|
|
6151
6195
|
detectInstalled,
|
|
6152
6196
|
loginShellResolver,
|
|
6153
6197
|
CodexAppServerDown,
|
|
6154
|
-
CodexAppServer
|
|
6198
|
+
CodexAppServer,
|
|
6199
|
+
loginShell,
|
|
6200
|
+
codexVersions,
|
|
6201
|
+
resolveCodexHome
|
|
6155
6202
|
};
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
browseDir,
|
|
23
23
|
childEnv,
|
|
24
24
|
cloneOfUnit,
|
|
25
|
+
codexVersions,
|
|
25
26
|
createWithFreshId,
|
|
26
27
|
createWorkspace,
|
|
27
28
|
describeStep,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
loadConfig,
|
|
42
43
|
loadOrCreateAgentKeys,
|
|
43
44
|
locateAgentEnv,
|
|
45
|
+
loginShell,
|
|
44
46
|
loginShellResolver,
|
|
45
47
|
mainCheckout,
|
|
46
48
|
migrateStateDir,
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
planCli,
|
|
54
56
|
planSetup,
|
|
55
57
|
renderService,
|
|
58
|
+
resolveCodexHome,
|
|
56
59
|
resolveStateDir,
|
|
57
60
|
revokeDevice,
|
|
58
61
|
sanitizeDeviceName,
|
|
@@ -63,7 +66,7 @@ import {
|
|
|
63
66
|
unitRunsLauncher,
|
|
64
67
|
withEnrolmentMode,
|
|
65
68
|
withLegacyEnv
|
|
66
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-KTUXNG4Q.js";
|
|
67
70
|
|
|
68
71
|
// src/cli.ts
|
|
69
72
|
import { randomBytes } from "node:crypto";
|
|
@@ -241,10 +244,9 @@ async function readOrNull(path) {
|
|
|
241
244
|
throw err;
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
|
-
function hooksFiles(home, hooksDir, port) {
|
|
247
|
+
function hooksFiles(home, hooksDir, port, codexHome) {
|
|
245
248
|
const settingsPath = join(home, ".claude", "settings.json");
|
|
246
|
-
const
|
|
247
|
-
const codexHooksPath = join(codexDir, "hooks.json");
|
|
249
|
+
const codexHooksPath = join(codexHome, "hooks.json");
|
|
248
250
|
async function wanted() {
|
|
249
251
|
const files = [];
|
|
250
252
|
for (const script of SCRIPTS) {
|
|
@@ -254,7 +256,7 @@ function hooksFiles(home, hooksDir, port) {
|
|
|
254
256
|
const settings = await readOrNull(settingsPath);
|
|
255
257
|
const merged = installHooks({ settings: settings === null ? {} : JSON.parse(settings) }, { hookCommand: HOOK_COMMAND, guardCommand: GUARD_COMMAND, port });
|
|
256
258
|
files.push({ path: settingsPath, current: settings, text: JSON.stringify(merged, null, 2) });
|
|
257
|
-
if (await fsIsDirectory(
|
|
259
|
+
if (await fsIsDirectory(codexHome)) {
|
|
258
260
|
const codex = await readOrNull(codexHooksPath);
|
|
259
261
|
const codexMerged = installHooks(
|
|
260
262
|
{ settings: codex === null ? {} : JSON.parse(codex) },
|
|
@@ -344,7 +346,7 @@ async function apply(step, effects) {
|
|
|
344
346
|
await effects.createKey();
|
|
345
347
|
return describeStep(step, "").replace(/^create/, "created");
|
|
346
348
|
case "write-roster":
|
|
347
|
-
await effects.writeRoster(
|
|
349
|
+
await effects.writeRoster();
|
|
348
350
|
return describeStep(step, "").replace(/^write/, "wrote");
|
|
349
351
|
case "stop-legacy-service":
|
|
350
352
|
return `stopped the old agent service: ${(await effects.stopLegacyService()).join("; ")}`;
|
|
@@ -541,6 +543,10 @@ function findMise(home, pathVar, exists2 = existsSync) {
|
|
|
541
543
|
const candidates = [join2(home, ".local", "bin", "mise"), "/opt/homebrew/bin/mise", "/usr/local/bin/mise", ...(pathVar ?? "").split(delimiter).filter(Boolean).map((dir) => join2(dir, "mise"))];
|
|
542
544
|
return candidates.find(exists2) ?? null;
|
|
543
545
|
}
|
|
546
|
+
function findInstalledCli(install, exists2 = existsSync) {
|
|
547
|
+
if (!install) return null;
|
|
548
|
+
return [join2(install, "bin", "tarfa"), join2(install, "node_modules", "@tarfa", "cli", "dist", "cli.js")].find(exists2) ?? null;
|
|
549
|
+
}
|
|
544
550
|
function miseCli(home) {
|
|
545
551
|
const run4 = async (mise, args, timeout) => {
|
|
546
552
|
try {
|
|
@@ -555,9 +561,9 @@ function miseCli(home) {
|
|
|
555
561
|
async locate(mise, version) {
|
|
556
562
|
const install = await run4(mise, ["where", `npm:@tarfa/cli@${version}`], 6e4);
|
|
557
563
|
const nodeHome = await run4(mise, ["where", "node"], 6e4);
|
|
558
|
-
const cli =
|
|
564
|
+
const cli = findInstalledCli(install);
|
|
559
565
|
const node = join2(nodeHome, "bin", "node");
|
|
560
|
-
if (!
|
|
566
|
+
if (!cli) throw new Error(`mise knows no install of @tarfa/cli ${version} with a bin/tarfa or node_modules/@tarfa/cli/dist/cli.js`);
|
|
561
567
|
if (!nodeHome || !existsSync(node)) throw new Error("mise knows no node");
|
|
562
568
|
return { node, cli };
|
|
563
569
|
},
|
|
@@ -909,7 +915,7 @@ var systemDeps = {
|
|
|
909
915
|
exec: async (command, args) => (await run2(command, args, { env: childEnv() })).stdout,
|
|
910
916
|
readFile: (path) => readFile5(path, "utf8")
|
|
911
917
|
};
|
|
912
|
-
var AGENT_COMMAND = /\bsrc\/serve\.ts\b|(^|[\s/])tarfa serve(\s|$)/;
|
|
918
|
+
var AGENT_COMMAND = /\bsrc\/serve\.ts\b|(^|[\s/])tarfa serve(\s|$)|\/@tarfa\/cli\/dist\/cli\.js serve(\s|$)/;
|
|
913
919
|
function isFree(port) {
|
|
914
920
|
return new Promise((resolve2, reject) => {
|
|
915
921
|
const server = createServer();
|
|
@@ -1074,12 +1080,13 @@ async function spawnCodex(deps, input) {
|
|
|
1074
1080
|
firstMessage: input.brief.replace(/\n+$/, "")
|
|
1075
1081
|
});
|
|
1076
1082
|
if (!/^[0-9A-Za-z-]+$/.test(thread)) throw new Error(`the Codex app server named an unexpected thread id: ${thread}`);
|
|
1083
|
+
const codexHome = await deps.codex.home();
|
|
1077
1084
|
const lockTitle = await titleLock(deps, title);
|
|
1078
1085
|
const command = `codex resume ${thread}`;
|
|
1079
1086
|
const id = await createWithFreshId(
|
|
1080
1087
|
deps.ids,
|
|
1081
1088
|
cwd,
|
|
1082
|
-
(id2) => deps.tmux.createSession({ name: id2, cwd, command, codexThread: thread, ...parent ? { parent: parent.id } : {} })
|
|
1089
|
+
(id2) => deps.tmux.createSession({ name: id2, cwd, command, codexThread: thread, env: { CODEX_HOME: codexHome }, ...parent ? { parent: parent.id } : {} })
|
|
1083
1090
|
);
|
|
1084
1091
|
return finishLaunch(deps, id, title, lockTitle, ` (Codex thread ${thread})`);
|
|
1085
1092
|
}
|
|
@@ -1129,35 +1136,6 @@ function denySettings(denyPath) {
|
|
|
1129
1136
|
};
|
|
1130
1137
|
}
|
|
1131
1138
|
|
|
1132
|
-
// src/infrastructure/codex-versions.ts
|
|
1133
|
-
import { execFile as execFile6 } from "node:child_process";
|
|
1134
|
-
function loginShell(shell = process.env.SHELL || "/bin/sh") {
|
|
1135
|
-
return (script) => new Promise((resolve2, reject) => {
|
|
1136
|
-
execFile6(shell, ["-lc", script], { env: childEnv(), timeout: 15e3 }, (err, stdout) => err ? reject(err) : resolve2(stdout));
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
function codexVersions(run4 = loginShell()) {
|
|
1140
|
-
return async () => {
|
|
1141
|
-
const [cliOut, daemonOut] = await Promise.all([
|
|
1142
|
-
run4("codex --version").catch((err) => {
|
|
1143
|
-
throw new Error(`cannot run codex --version: ${err.message}`);
|
|
1144
|
-
}),
|
|
1145
|
-
run4("codex app-server daemon version").catch((err) => {
|
|
1146
|
-
throw new Error(`cannot read the Codex app server's version: ${err.message}`);
|
|
1147
|
-
})
|
|
1148
|
-
]);
|
|
1149
|
-
const cli = cliOut.trim().split(/\s+/).pop() ?? "";
|
|
1150
|
-
let appServer;
|
|
1151
|
-
try {
|
|
1152
|
-
appServer = JSON.parse(daemonOut).appServerVersion;
|
|
1153
|
-
} catch {
|
|
1154
|
-
appServer = void 0;
|
|
1155
|
-
}
|
|
1156
|
-
if (typeof appServer !== "string" || appServer === "") throw new Error("the Codex app server is not running");
|
|
1157
|
-
return { cli, appServer };
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
1139
|
// src/application/workspace-role.ts
|
|
1162
1140
|
var WORKSPACE_ROLES = ["orchestrator", "child", "normal"];
|
|
1163
1141
|
function workspaceRole(w, all) {
|
|
@@ -1262,7 +1240,7 @@ async function run3(argv, rawEnv, home, io = {}) {
|
|
|
1262
1240
|
return JSON.stringify(await browseDir({ lister, hidden }, path, config.projectRoots), null, 2);
|
|
1263
1241
|
}
|
|
1264
1242
|
case "install-hooks": {
|
|
1265
|
-
const hooks = hooksFiles(home, HOOKS_DIR, config.port);
|
|
1243
|
+
const hooks = hooksFiles(home, HOOKS_DIR, config.port, await resolveCodexHome({ ...env, ...await agentEnvValues(env, home) }, home, io.loginShell));
|
|
1266
1244
|
const { codex } = await hooks.install();
|
|
1267
1245
|
if (!codex) return JSON.stringify({ installed: hooks.settingsPath, scriptPath: hooks.scriptPath });
|
|
1268
1246
|
return JSON.stringify({
|
|
@@ -1283,7 +1261,7 @@ var HELP = `usage: tarfa <command> [options] (\`tarfa setup --help\`, \`tarfa
|
|
|
1283
1261
|
Set up this machine:
|
|
1284
1262
|
setup write agent.env, the agent key, the service and the hooks, then check the phone port
|
|
1285
1263
|
pair print a one-time pairing link and QR code for a phone
|
|
1286
|
-
devices list, revoke or forget paired phones;
|
|
1264
|
+
devices list, revoke or forget paired phones; open or close enrolment
|
|
1287
1265
|
update install the newest @tarfa/cli and apply it (package installs)
|
|
1288
1266
|
apply rewrite the launcher and service if they changed, reinstall the hooks, restart
|
|
1289
1267
|
install-hooks install the Claude Code and Codex hooks that report each workspace's status
|
|
@@ -1409,7 +1387,7 @@ async function orchestration(command, argv, env, home, io) {
|
|
|
1409
1387
|
nonce: () => randomBytes(6).toString("hex"),
|
|
1410
1388
|
sleep: io.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
|
|
1411
1389
|
spawnDir: joinPath(config.stateDir, "spawn"),
|
|
1412
|
-
codex: io.codex ?? {
|
|
1390
|
+
codex: io.codex ?? codexSpawn({ ...env, ...values }, home, io)
|
|
1413
1391
|
},
|
|
1414
1392
|
{
|
|
1415
1393
|
projectPath,
|
|
@@ -1629,6 +1607,15 @@ function pwaOrigin(value) {
|
|
|
1629
1607
|
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`--pwa must be an http(s) origin (got ${value})`);
|
|
1630
1608
|
return url.origin;
|
|
1631
1609
|
}
|
|
1610
|
+
function codexSpawn(env, home, io) {
|
|
1611
|
+
let codexHome;
|
|
1612
|
+
const resolve2 = () => codexHome ??= resolveCodexHome(env, home, io.loginShell);
|
|
1613
|
+
return {
|
|
1614
|
+
home: resolve2,
|
|
1615
|
+
versions: async () => codexVersions(loginShell(void 0, { CODEX_HOME: await resolve2() }))(),
|
|
1616
|
+
createThread: async (input) => new CodexAppServer({ codexHome: await resolve2() }).createThread(input)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1632
1619
|
async function agentEnvValues(env, home) {
|
|
1633
1620
|
try {
|
|
1634
1621
|
return withLegacyEnv(parseEnvFile(await readFile6(await locateAgentEnv(env, home), "utf8")));
|
|
@@ -1684,7 +1671,7 @@ agent key, a new device roster that admits pairing codes only (an existing one i
|
|
|
1684
1671
|
user unit with linger, or launchd), the Claude Code hooks, and optionally a Tailscale address for
|
|
1685
1672
|
phones. Rerunning it with the same flags, or with none, changes nothing: --machine-id, --project-roots,
|
|
1686
1673
|
--phone-port and --default-path default to what the agent.env it finds holds. It refuses rather than
|
|
1687
|
-
replace anything it did not make (an agent key,
|
|
1674
|
+
replace anything it did not make (an agent key, another tailscale serve entry).
|
|
1688
1675
|
|
|
1689
1676
|
--machine-id <id> the name phones show for this machine
|
|
1690
1677
|
--project-roots <dir,dir> absolute folders new workspaces may be made in
|
|
@@ -1700,7 +1687,6 @@ replace anything it did not make (an agent key, a token, another tailscale serve
|
|
|
1700
1687
|
4 in all), e.g. a reverse proxy
|
|
1701
1688
|
--pwa <origin> where phones open the app (default https://tarfa.app; an existing
|
|
1702
1689
|
TARFA_PWA_ORIGIN is kept unless this is given)
|
|
1703
|
-
--token-enrolment on|off let TARFA_TOKEN enrol phones (off by default on a new roster)
|
|
1704
1690
|
--pair print a pairing link and QR code at the end
|
|
1705
1691
|
--adopt a machine whose service runs a clone's deploy/run-agent.sh: move it to
|
|
1706
1692
|
the installed package (copies deploy/agent.env to ~/.tarfa/agent.env,
|
|
@@ -1722,8 +1708,7 @@ Several agents on one node (one per Linux user) share a Tailscale port, each und
|
|
|
1722
1708
|
The node has one operator: a second user runs \`--tailscale none --slug <name> --address <url>\`
|
|
1723
1709
|
with their own --phone-port, and setup prints the \`tailscale serve\` line for the operator to run:
|
|
1724
1710
|
on the port of that --address (or --tailscale-port), as \`tailscale funnel\` when the port is already
|
|
1725
|
-
on Funnel.
|
|
1726
|
-
--token-enrolment says which way. Run as another user (sudo -u), set XDG_RUNTIME_DIR=/run/user/<uid>.
|
|
1711
|
+
on Funnel. Run as another user (sudo -u), set XDG_RUNTIME_DIR=/run/user/<uid>.
|
|
1727
1712
|
|
|
1728
1713
|
Related: \`tarfa apply\` brings the service and hooks in line with the installed package and checks
|
|
1729
1714
|
the agent came back; \`tarfa update\` installs a newer release with mise, then runs its apply.
|
|
@@ -1788,7 +1773,7 @@ ${SETUP_USAGE.split("\n")[0]}
|
|
|
1788
1773
|
const config = loadConfig({ ...withLegacyEnv(parseEnvFile(envText ?? "")), TARFA_MACHINE_ID: flags.machineId }, home);
|
|
1789
1774
|
const tailscale = fx.tailscale ?? tailscaleCli();
|
|
1790
1775
|
const owner = fx.portOwner ?? portOwner;
|
|
1791
|
-
const hooks = hooksFiles(home, HOOKS_DIR, config.port);
|
|
1776
|
+
const hooks = hooksFiles(home, HOOKS_DIR, config.port, await resolveCodexHome({ ...env, ...withLegacyEnv(parseEnvFile(envText ?? "")) }, home, io.loginShell));
|
|
1792
1777
|
const devices2 = new FileDeviceStore(config.devicesPath);
|
|
1793
1778
|
const hasRoster = await readFile6(config.devicesPath).then(() => true, () => false);
|
|
1794
1779
|
const agentLivePath = cloneRepo === null ? "" : joinPath(cloneRepo, "deploy", "agent-live");
|
|
@@ -1803,7 +1788,6 @@ ${SETUP_USAGE.split("\n")[0]}
|
|
|
1803
1788
|
service: { kind: service.kind, template: await service.template(), installed: installedUnit, enabled: await service.enabled() },
|
|
1804
1789
|
envPath,
|
|
1805
1790
|
envText,
|
|
1806
|
-
newToken: randomBytes(32).toString("hex"),
|
|
1807
1791
|
hasKey: await readFile6(config.agentKeyPath).then(() => true, () => false),
|
|
1808
1792
|
roster: hasRoster ? enrolmentMode(await devices2.read()) : "absent",
|
|
1809
1793
|
hooksCurrent: await hooks.current(),
|
|
@@ -1828,9 +1812,9 @@ ${SETUP_USAGE.split("\n")[0]}
|
|
|
1828
1812
|
createKey: async () => {
|
|
1829
1813
|
await loadOrCreateAgentKeys(config.agentKeyPath);
|
|
1830
1814
|
},
|
|
1831
|
-
writeRoster: async (
|
|
1815
|
+
writeRoster: async () => {
|
|
1832
1816
|
await mkdir4(dirname4(config.devicesPath), { recursive: true, mode: 448 });
|
|
1833
|
-
await devices2.update((roster) =>
|
|
1817
|
+
await devices2.update((roster) => roster);
|
|
1834
1818
|
},
|
|
1835
1819
|
writeLauncher: (path, text) => atomicWrite(path, text, 493),
|
|
1836
1820
|
copyEnv: (from, to) => copyEnvOnce(from, to),
|
|
@@ -1915,7 +1899,7 @@ async function apply2(argv, env, home, io) {
|
|
|
1915
1899
|
if (envText === null) throw new Error(`failed: check: no agent config at ${envPath}: run tarfa setup`);
|
|
1916
1900
|
const config = loadConfig(withLegacyEnv(parseEnvFile(envText)), home);
|
|
1917
1901
|
const service = fx.service ?? serviceInstall({ home, user: userInfo().username, uid: process.getuid?.() ?? 0 });
|
|
1918
|
-
const hooks = hooksFiles(home, HOOKS_DIR, config.port);
|
|
1902
|
+
const hooks = hooksFiles(home, HOOKS_DIR, config.port, await resolveCodexHome({ ...env, ...withLegacyEnv(parseEnvFile(envText)) }, home, io.loginShell));
|
|
1919
1903
|
const launcherPath = joinPath(home, ".tarfa", "bin", "tarfa-agent");
|
|
1920
1904
|
const log = fx.agentLog ?? agentLog({ home });
|
|
1921
1905
|
const result = await runApply(
|
|
@@ -1977,14 +1961,12 @@ function pairingInstructions(link, minutes = PAIRING_TTL_MS / 6e4) {
|
|
|
1977
1961
|
].join("\n");
|
|
1978
1962
|
}
|
|
1979
1963
|
var DEVICES_USAGE = [
|
|
1980
|
-
"usage: tarfa devices [list | revoke <id|key> | forget <id|key> | enrolment
|
|
1981
|
-
" enrolment
|
|
1982
|
-
" enrolment off only a pairing code from `tarfa pair` enrols one",
|
|
1964
|
+
"usage: tarfa devices [list | revoke <id|key> | forget <id|key> | enrolment open|closed]",
|
|
1965
|
+
" enrolment open a pairing code from `tarfa pair` enrols a new phone",
|
|
1983
1966
|
" enrolment closed no new phone can enrol"
|
|
1984
1967
|
].join("\n");
|
|
1985
1968
|
var ENROLMENT_MODES = {
|
|
1986
|
-
|
|
1987
|
-
off: "off (only a pairing code from `tarfa pair` enrols a phone)",
|
|
1969
|
+
open: "open (a pairing code from `tarfa pair` enrols a phone)",
|
|
1988
1970
|
closed: "closed (no new phone can enrol)"
|
|
1989
1971
|
};
|
|
1990
1972
|
async function devices(store, [action = "list", arg]) {
|
|
@@ -1995,23 +1977,19 @@ async function devices(store, [action = "list", arg]) {
|
|
|
1995
1977
|
case "forget": {
|
|
1996
1978
|
if (!arg) throw new Error(DEVICES_USAGE);
|
|
1997
1979
|
const found = { device: null };
|
|
1998
|
-
|
|
1980
|
+
await store.update((roster) => {
|
|
1999
1981
|
const result = action === "revoke" ? revokeDevice(roster, arg, /* @__PURE__ */ new Date()) : forgetDevice(roster, arg);
|
|
2000
1982
|
found.device = result.device;
|
|
2001
1983
|
return result.roster;
|
|
2002
1984
|
});
|
|
2003
1985
|
const { device } = found;
|
|
2004
1986
|
if (!device) throw new Error(`no single device matches ${arg}; see tarfa devices, and name it by its full key if its id is shared`);
|
|
2005
|
-
if (action === "forget") return `forgot ${device.id} (${sanitizeDeviceName(device.name)}); it can enrol again
|
|
2006
|
-
|
|
2007
|
-
if (enrolmentMode(written) === "on") {
|
|
2008
|
-
lines.push("enrolment is on: whoever holds the token can still enrol a new key.");
|
|
2009
|
-
lines.push("Run `tarfa devices enrolment off`, or rotate the token, to stop that.");
|
|
2010
|
-
}
|
|
2011
|
-
return lines.join("\n");
|
|
1987
|
+
if (action === "forget") return `forgot ${device.id} (${sanitizeDeviceName(device.name)}); it can enrol again with a \`tarfa pair\` code`;
|
|
1988
|
+
return `revoked ${device.id} (${sanitizeDeviceName(device.name)}); this key stays refused until forgotten`;
|
|
2012
1989
|
}
|
|
2013
1990
|
case "enrolment": {
|
|
2014
|
-
if (arg
|
|
1991
|
+
if (arg === "on" || arg === "off") throw new Error(`token enrolment is gone: phones join only with a \`tarfa pair\` code. Use \`enrolment open\` or \`enrolment closed\``);
|
|
1992
|
+
if (arg !== "open" && arg !== "closed") throw new Error(DEVICES_USAGE);
|
|
2015
1993
|
await store.update((roster) => withEnrolmentMode(roster, arg));
|
|
2016
1994
|
return `enrolment: ${ENROLMENT_MODES[arg]}`;
|
|
2017
1995
|
}
|
package/dist/serve.js
CHANGED
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
ShortWorkspaceIds,
|
|
26
26
|
TmuxAdapter,
|
|
27
27
|
WrongStaticKeyError,
|
|
28
|
-
assertServerConfig,
|
|
29
28
|
audioMediaType,
|
|
30
29
|
b64url,
|
|
31
30
|
browseDir,
|
|
@@ -55,7 +54,6 @@ import {
|
|
|
55
54
|
loadOrCreateAgentKeys,
|
|
56
55
|
locateAgentEnv,
|
|
57
56
|
loginShellResolver,
|
|
58
|
-
mayEnrol,
|
|
59
57
|
migrateStateDir,
|
|
60
58
|
packageVersion,
|
|
61
59
|
parseBrowserAgentMessage,
|
|
@@ -67,6 +65,7 @@ import {
|
|
|
67
65
|
readHandshakeFrame,
|
|
68
66
|
redeemPairing,
|
|
69
67
|
relationOf,
|
|
68
|
+
resolveCodexHome,
|
|
70
69
|
runsClaude,
|
|
71
70
|
sessionOfCodexThread,
|
|
72
71
|
standingOf,
|
|
@@ -75,7 +74,7 @@ import {
|
|
|
75
74
|
systemPathResolver,
|
|
76
75
|
withLegacyEnv,
|
|
77
76
|
writeFileAtomic
|
|
78
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-KTUXNG4Q.js";
|
|
79
78
|
|
|
80
79
|
// src/serve.ts
|
|
81
80
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -1324,7 +1323,7 @@ var NoiseClientConnection = class {
|
|
|
1324
1323
|
return this.keyAuth !== null && this.state.kind !== "failed" ? this.keyAuth : void 0;
|
|
1325
1324
|
}
|
|
1326
1325
|
// An accepted `auth` is answered with whether the device key is now enrolled: the phone drops
|
|
1327
|
-
// its
|
|
1326
|
+
// its pairing secret on `enrolled`, so for a new device the answer waits for the
|
|
1328
1327
|
// roster to be written.
|
|
1329
1328
|
send(message) {
|
|
1330
1329
|
if (!this.session || this.state.kind === "failed") return;
|
|
@@ -1451,7 +1450,7 @@ var NoiseClientConnection = class {
|
|
|
1451
1450
|
return;
|
|
1452
1451
|
}
|
|
1453
1452
|
this.keyAuth = standing === "enrolled" ? "device" : null;
|
|
1454
|
-
this.state = standing === "enrolled" ? { kind: "open" } : { kind: "enrolling", hello
|
|
1453
|
+
this.state = standing === "enrolled" ? { kind: "open" } : { kind: "enrolling", hello };
|
|
1455
1454
|
if (standing === "enrolled" && hello.push && this.keyAuthenticatedListeners.length > 0 && this.options.pushBudget?.take(key)) {
|
|
1456
1455
|
this.awaitingPhone = true;
|
|
1457
1456
|
this.keyAuthenticatedListeners.forEach((l) => l());
|
|
@@ -1509,16 +1508,16 @@ var NoiseClientConnection = class {
|
|
|
1509
1508
|
route(raw) {
|
|
1510
1509
|
if (this.state.kind === "failed") return;
|
|
1511
1510
|
const message = raw;
|
|
1512
|
-
|
|
1511
|
+
const pairing = message?.type === "auth" && typeof message.token === "string" && PAIRING_SECRET.test(message.token);
|
|
1512
|
+
if (this.state.kind === "enrolling" && !pairing) {
|
|
1513
|
+
this.logRefusal("no-code", `phone: device ${deviceFingerprint(this.deviceKey)} not enrolled and sent no pairing code`);
|
|
1513
1514
|
this.fail(REFUSAL.notEnrolled, DEVICE_REFUSED);
|
|
1514
1515
|
return;
|
|
1515
1516
|
}
|
|
1516
|
-
const pairing = message?.type === "auth" && typeof message.token === "string" && PAIRING_SECRET.test(message.token);
|
|
1517
1517
|
if (pairing && (this.state.kind === "enrolling" || this.keyAuth !== null)) {
|
|
1518
1518
|
this.spendPairing(message.token, raw);
|
|
1519
1519
|
return;
|
|
1520
1520
|
}
|
|
1521
|
-
if (this.refusesToken()) return;
|
|
1522
1521
|
this.listeners.forEach((l) => l(raw));
|
|
1523
1522
|
}
|
|
1524
1523
|
spendPairing(secret, auth) {
|
|
@@ -1531,12 +1530,12 @@ var NoiseClientConnection = class {
|
|
|
1531
1530
|
const held = this.held ?? [];
|
|
1532
1531
|
this.held = null;
|
|
1533
1532
|
if (this.state.kind === "failed") return;
|
|
1534
|
-
if (!redemption.redeemed && redemption.reason) {
|
|
1535
|
-
|
|
1536
|
-
this.
|
|
1533
|
+
if (!redemption.redeemed && (redemption.reason || this.keyAuth === null)) {
|
|
1534
|
+
const reason = redemption.reason ?? REFUSAL.notEnrolled;
|
|
1535
|
+
this.logRefusal(reason, `phone: device ${fingerprint} refused: ${reason}`);
|
|
1536
|
+
this.fail(reason, DEVICE_REFUSED);
|
|
1537
1537
|
return;
|
|
1538
1538
|
}
|
|
1539
|
-
if (!redemption.redeemed && this.refusesToken()) return;
|
|
1540
1539
|
this.listeners.forEach((l) => l(auth));
|
|
1541
1540
|
held.forEach((raw) => this.drain(raw));
|
|
1542
1541
|
};
|
|
@@ -1550,24 +1549,15 @@ var NoiseClientConnection = class {
|
|
|
1550
1549
|
);
|
|
1551
1550
|
}
|
|
1552
1551
|
}
|
|
1553
|
-
// An unknown device whose `auth` is about to be checked as a token, while only a pairing code
|
|
1554
|
-
// may enrol it: refused here, so the token never gets as far as the handler.
|
|
1555
|
-
refusesToken() {
|
|
1556
|
-
if (this.state.kind !== "enrolling" || this.state.tokenEnrols || this.keyAuth !== null) return false;
|
|
1557
|
-
this.logRefusal("token-off", `phone: device ${deviceFingerprint(this.deviceKey)} refused: token enrolment is off`);
|
|
1558
|
-
this.fail(REFUSAL.enrolmentOff, DEVICE_REFUSED);
|
|
1559
|
-
return true;
|
|
1560
|
-
}
|
|
1561
1552
|
// `declined`: the roster would not take the device (enrolment turned off or closed since the
|
|
1562
1553
|
// handshake, or its id taken), so the credential that authenticated this session no longer
|
|
1563
1554
|
// admits it. `failed`: the write itself failed; the session goes on unenrolled, and the phone
|
|
1564
1555
|
// keeps its credential to try again.
|
|
1565
1556
|
async enrol(hello) {
|
|
1566
1557
|
const key = this.deviceKey;
|
|
1567
|
-
const via = this.keyAuth === "pairing" ? "pairing" : "token";
|
|
1568
1558
|
this.state = { kind: "open" };
|
|
1569
1559
|
try {
|
|
1570
|
-
const enrolled = await this.options.devices.enrol({ x25519: key, ed25519: hello.sig, name: hello.name }
|
|
1560
|
+
const enrolled = await this.options.devices.enrol({ x25519: key, ed25519: hello.sig, name: hello.name });
|
|
1571
1561
|
this.options.log?.(
|
|
1572
1562
|
enrolled ? `phone: enrolled device ${deviceFingerprint(key)} "${hello.name}"` : `phone: device ${deviceFingerprint(key)} not enrolled (enrolment changed meanwhile, or its id is taken); closing its session`
|
|
1573
1563
|
);
|
|
@@ -1581,7 +1571,6 @@ var NoiseClientConnection = class {
|
|
|
1581
1571
|
try {
|
|
1582
1572
|
const { enrolment } = await this.options.devices.check(this.deviceKey);
|
|
1583
1573
|
if (enrolment === "closed") return REFUSAL.enrolmentClosed;
|
|
1584
|
-
if (enrolment === "off" && this.keyAuth !== "pairing") return REFUSAL.enrolmentOff;
|
|
1585
1574
|
} catch {
|
|
1586
1575
|
}
|
|
1587
1576
|
return REFUSAL.enrolmentRefused;
|
|
@@ -2305,17 +2294,11 @@ var ConnectionHandler = class {
|
|
|
2305
2294
|
return;
|
|
2306
2295
|
}
|
|
2307
2296
|
if (message.type === "auth") {
|
|
2308
|
-
const token = this.deps.config.token;
|
|
2309
|
-
const isToken = token !== void 0 && constantTimeEqual(message.token, token);
|
|
2310
|
-
const byKey = this.conn.keyCredential;
|
|
2311
|
-
if (byKey === "device" && isToken && this.conn.refuse) {
|
|
2312
|
-
this.conn.refuse(REFUSAL.tokenNotNeeded);
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
2297
|
if (this.authed) return;
|
|
2316
|
-
|
|
2298
|
+
const byKey = this.conn.keyCredential;
|
|
2299
|
+
if (byKey) {
|
|
2317
2300
|
this.authed = true;
|
|
2318
|
-
this.conn.send({ type: "authResult", ok: true, credential: byKey
|
|
2301
|
+
this.conn.send({ type: "authResult", ok: true, credential: byKey });
|
|
2319
2302
|
} else {
|
|
2320
2303
|
this.conn.send({ type: "authResult", ok: false });
|
|
2321
2304
|
this.conn.close();
|
|
@@ -3792,23 +3775,23 @@ var RosterDevices = class {
|
|
|
3792
3775
|
const roster = await this.deps.store.read();
|
|
3793
3776
|
return { standing: standingOf(roster, x25519), enrolment: enrolmentMode(roster) };
|
|
3794
3777
|
}
|
|
3795
|
-
async enrol(device
|
|
3778
|
+
async enrol(device) {
|
|
3796
3779
|
let known = true;
|
|
3797
3780
|
const roster = await this.deps.store.update((current) => {
|
|
3798
3781
|
known = standingOf(current, device.x25519) !== "unknown";
|
|
3799
|
-
return
|
|
3782
|
+
return current.enrolment ? enrolDevice(current, device, this.deps.now()) : current;
|
|
3800
3783
|
});
|
|
3801
3784
|
this.deps.onChange();
|
|
3802
3785
|
const entry = known ? void 0 : roster.devices.find((d) => d.x25519 === device.x25519);
|
|
3803
3786
|
try {
|
|
3804
|
-
if (entry) this.deps.onEnrolled?.({ id: entry.id, name: entry.name, at: entry.enrolledAt, x25519: entry.x25519 }
|
|
3787
|
+
if (entry) this.deps.onEnrolled?.({ id: entry.id, name: entry.name, at: entry.enrolledAt, x25519: entry.x25519 });
|
|
3805
3788
|
} catch {
|
|
3806
3789
|
}
|
|
3807
3790
|
return standingOf(roster, device.x25519) === "enrolled";
|
|
3808
3791
|
}
|
|
3809
3792
|
// Spends a pairing secret for the device (X25519 key) about to be enrolled, binding it to that
|
|
3810
3793
|
// device. Atomic across processes, so two phones racing with one code cannot both get in. A
|
|
3811
|
-
// secret this agent never issued (or has forgotten) gives no reason
|
|
3794
|
+
// secret this agent never issued (or has forgotten) gives no reason.
|
|
3812
3795
|
async redeemPairing(secret, device) {
|
|
3813
3796
|
if (!(await this.deps.store.read()).enrolment) return { redeemed: false, reason: REFUSAL.enrolmentClosed };
|
|
3814
3797
|
const outcome = await this.spend(secret, device);
|
|
@@ -3870,8 +3853,8 @@ function startDeviceGate(deps) {
|
|
|
3870
3853
|
pairings: deps.pairings,
|
|
3871
3854
|
now: () => /* @__PURE__ */ new Date(),
|
|
3872
3855
|
onChange: wrote,
|
|
3873
|
-
onEnrolled: (device
|
|
3874
|
-
log(`device enrolled: ${device.id} "${device.name}" (
|
|
3856
|
+
onEnrolled: (device) => {
|
|
3857
|
+
log(`device enrolled: ${device.id} "${device.name}" (pairing code)`);
|
|
3875
3858
|
server.broadcastDeviceEnrolled(device);
|
|
3876
3859
|
}
|
|
3877
3860
|
});
|
|
@@ -4041,7 +4024,7 @@ function limitedDevices(devices, limiter, roster) {
|
|
|
4041
4024
|
if (known && standingOf(known, x25519) === "enrolled") return { standing: "enrolled", enrolment: enrolmentMode(known) };
|
|
4042
4025
|
return limited(limiter, () => devices.check(x25519));
|
|
4043
4026
|
},
|
|
4044
|
-
enrol: (device
|
|
4027
|
+
enrol: (device) => devices.enrol(device),
|
|
4045
4028
|
redeemPairing: (secret, device) => limited(limiter, () => devices.redeemPairing(secret, device)),
|
|
4046
4029
|
burnPairing: (secret, device) => devices.burnPairing(secret, device)
|
|
4047
4030
|
};
|
|
@@ -4202,8 +4185,10 @@ async function main() {
|
|
|
4202
4185
|
}
|
|
4203
4186
|
const env = withLegacyEnv(process.env);
|
|
4204
4187
|
const config = loadConfig(env, homedir2());
|
|
4205
|
-
assertServerConfig(config);
|
|
4206
4188
|
for (const warning of configWarnings(process.env)) process.stderr.write(`${warning}
|
|
4189
|
+
`);
|
|
4190
|
+
const codexHome = await resolveCodexHome(env, homedir2());
|
|
4191
|
+
process.stdout.write(`tarfa agent: Codex home ${codexHome}
|
|
4207
4192
|
`);
|
|
4208
4193
|
await securePrivateDir(config.stateDir);
|
|
4209
4194
|
await assertPrivateFiles([
|
|
@@ -4211,7 +4196,7 @@ async function main() {
|
|
|
4211
4196
|
config.devicesPath,
|
|
4212
4197
|
config.pairingPath,
|
|
4213
4198
|
join7(config.stateDir, "token"),
|
|
4214
|
-
//
|
|
4199
|
+
// The launcher sources it: whoever can write it runs code as this user.
|
|
4215
4200
|
await locateAgentEnv(env, homedir2())
|
|
4216
4201
|
]);
|
|
4217
4202
|
const hookToken = await writeHookToken(config.stateDir);
|
|
@@ -4260,7 +4245,7 @@ async function main() {
|
|
|
4260
4245
|
delivery: new CompositeDelivery({
|
|
4261
4246
|
claude: new ClaudeInboxDelivery(claudeSessions, async (id) => await tmux.firstPaneId?.(id) ?? null),
|
|
4262
4247
|
pi: new PiInboxDelivery(config.inboxDir),
|
|
4263
|
-
codex: new CodexDelivery(new CodexAppServer())
|
|
4248
|
+
codex: new CodexDelivery(new CodexAppServer({ codexHome }))
|
|
4264
4249
|
}),
|
|
4265
4250
|
log: new FileMessageLog(config.messageLogPath)
|
|
4266
4251
|
},
|
|
@@ -4283,10 +4268,12 @@ async function main() {
|
|
|
4283
4268
|
const keys = await loadOrCreateAgentKeys(config.agentKeyPath);
|
|
4284
4269
|
const log = (message) => process.stdout.write(`${message}
|
|
4285
4270
|
`);
|
|
4271
|
+
const devices = new FileDeviceStore(config.devicesPath);
|
|
4272
|
+
if (await devices.sealTokenEnrolment()) log("tarfa agent: wrote token enrolment off into devices.json (phones join with `tarfa pair` only)");
|
|
4286
4273
|
const gate = startDeviceGate({
|
|
4287
4274
|
keys,
|
|
4288
4275
|
server,
|
|
4289
|
-
store:
|
|
4276
|
+
store: devices,
|
|
4290
4277
|
pairings: new FilePairingStore(config.pairingPath),
|
|
4291
4278
|
log,
|
|
4292
4279
|
rosterPollMs: ROSTER_POLL_MS,
|
package/package.json
CHANGED
package/templates/tarfa-agent.sh
CHANGED
|
@@ -40,8 +40,17 @@ for mise_bin in "$home/.local/bin/mise" /opt/homebrew/bin/mise /usr/local/bin/mi
|
|
|
40
40
|
if [ -x "$mise_bin" ]; then
|
|
41
41
|
install="$("$mise_bin" --cd "$home" where npm:@tarfa/cli 2>/dev/null || true)"
|
|
42
42
|
node_home="$("$mise_bin" --cd "$home" where node 2>/dev/null || true)"
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# npm links the CLI into <install>/bin; aube (a newer mise's npm backend) leaves it in the
|
|
44
|
+
# package under node_modules, whose .bin/tarfa is a shell script node cannot run.
|
|
45
|
+
cli=""
|
|
46
|
+
for candidate in "$install/bin/tarfa" "$install/node_modules/@tarfa/cli/dist/cli.js"; do
|
|
47
|
+
if [ -n "$install" ] && [ -f "$candidate" ]; then
|
|
48
|
+
cli="$candidate"
|
|
49
|
+
break
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
if [ -n "$cli" ] && [ -n "$node_home" ] && [ -x "$node_home/bin/node" ]; then
|
|
53
|
+
exec "$node_home/bin/node" "$cli" serve
|
|
45
54
|
fi
|
|
46
55
|
exec "$mise_bin" exec -- tarfa serve
|
|
47
56
|
fi
|
|
@@ -22,8 +22,17 @@ fi
|
|
|
22
22
|
if [ -n "$mise_bin" ]; then
|
|
23
23
|
install="$("$mise_bin" --cd "$HOME" where npm:@tarfa/cli 2>/dev/null || true)"
|
|
24
24
|
node_home="$("$mise_bin" --cd "$HOME" where node 2>/dev/null || true)"
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# npm links the CLI into <install>/bin; aube (a newer mise's npm backend) leaves it in the
|
|
26
|
+
# package under node_modules, whose .bin/tarfa is a shell script node cannot run.
|
|
27
|
+
cli=""
|
|
28
|
+
for candidate in "$install/bin/tarfa" "$install/node_modules/@tarfa/cli/dist/cli.js"; do
|
|
29
|
+
if [ -n "$install" ] && [ -f "$candidate" ]; then
|
|
30
|
+
cli="$candidate"
|
|
31
|
+
break
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
if [ -n "$cli" ] && [ -n "$node_home" ] && [ -x "$node_home/bin/node" ]; then
|
|
35
|
+
exec "$node_home/bin/node" "$cli" "$@"
|
|
27
36
|
fi
|
|
28
37
|
fi
|
|
29
38
|
|