@songsid/agend 2.1.6-beta.1 → 2.1.6-beta.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/dist/apply-job.d.ts +114 -0
- package/dist/apply-job.js +214 -0
- package/dist/apply-job.js.map +1 -0
- package/dist/backend/codex.d.ts +24 -0
- package/dist/backend/codex.js +102 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +159 -0
- package/dist/backend/credential-profile.js +340 -0
- package/dist/backend/credential-profile.js.map +1 -0
- package/dist/backend/kiro-auth-store.d.ts +25 -0
- package/dist/backend/kiro-auth-store.js +61 -0
- package/dist/backend/kiro-auth-store.js.map +1 -0
- package/dist/backend/kiro.d.ts +8 -0
- package/dist/backend/kiro.js +31 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +21 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +27 -1
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-level-config.d.ts +42 -0
- package/dist/fleet-level-config.js +78 -0
- package/dist/fleet-level-config.js.map +1 -0
- package/dist/fleet-lock.d.ts +21 -0
- package/dist/fleet-lock.js +42 -8
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +122 -1
- package/dist/fleet-manager.js +537 -83
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +145 -0
- package/dist/instance-config-impact.d.ts +55 -0
- package/dist/instance-config-impact.js +152 -0
- package/dist/instance-config-impact.js.map +1 -0
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -1
- package/dist/outbound-handlers.js +199 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +6 -5
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +45 -0
- package/dist/provider-probe.js +111 -0
- package/dist/provider-probe.js.map +1 -0
- package/dist/quickstart-api.d.ts +161 -0
- package/dist/quickstart-api.js +340 -0
- package/dist/quickstart-api.js.map +1 -0
- package/dist/quickstart.js +23 -50
- package/dist/quickstart.js.map +1 -1
- package/dist/self-restart-limit.d.ts +21 -0
- package/dist/self-restart-limit.js +129 -0
- package/dist/self-restart-limit.js.map +1 -0
- package/dist/settings-api.d.ts +15 -0
- package/dist/settings-api.js +126 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-auth.d.ts +113 -0
- package/dist/setup-auth.js +181 -0
- package/dist/setup-auth.js.map +1 -0
- package/dist/setup-form.d.ts +18 -0
- package/dist/setup-form.js +315 -0
- package/dist/setup-form.js.map +1 -0
- package/dist/setup-host.d.ts +162 -0
- package/dist/setup-host.js +496 -0
- package/dist/setup-host.js.map +1 -0
- package/dist/setup-marker.d.ts +8 -0
- package/dist/setup-marker.js +39 -0
- package/dist/setup-marker.js.map +1 -0
- package/dist/setup-tunnel-consent.d.ts +53 -0
- package/dist/setup-tunnel-consent.js +82 -0
- package/dist/setup-tunnel-consent.js.map +1 -0
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/transcript-sources.d.ts +12 -1
- package/dist/transcript-sources.js +19 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +71 -0
- package/dist/tunnel/cloudflared.js +447 -0
- package/dist/tunnel/cloudflared.js.map +1 -0
- package/dist/tunnel/lease.d.ts +75 -0
- package/dist/tunnel/lease.js +209 -0
- package/dist/tunnel/lease.js.map +1 -0
- package/dist/tunnel/manager.d.ts +56 -0
- package/dist/tunnel/manager.js +167 -0
- package/dist/tunnel/manager.js.map +1 -0
- package/dist/tunnel/types.d.ts +119 -0
- package/dist/tunnel/types.js +33 -0
- package/dist/tunnel/types.js.map +1 -0
- package/dist/ui/dashboard.html +4 -3
- package/dist/ui/settings.html +640 -153
- package/dist/usage/providers.d.ts +17 -14
- package/dist/usage/providers.js +87 -55
- package/dist/usage/providers.js.map +1 -1
- package/dist/web-api.js +6 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +75 -0
- package/dist/web-auth.js +208 -2
- package/dist/web-auth.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** How long a finished job stays answerable, and how long a key is honoured. */
|
|
2
|
+
export declare const APPLY_JOB_RETENTION_MS: number;
|
|
3
|
+
/**
|
|
4
|
+
* Wall clock, not a step count: the user is looking at a spinner, and the thing
|
|
5
|
+
* they need to be told is that it has been N seconds, not which internal phase
|
|
6
|
+
* it is in. Generous — a reconcile that restarts several agents on a loaded
|
|
7
|
+
* machine is slow but fine.
|
|
8
|
+
*/
|
|
9
|
+
export declare const APPLY_JOB_DEADLINE_MS = 120000;
|
|
10
|
+
/** Outcome of asking the fleet to restart itself for a Settings change. */
|
|
11
|
+
export type SelfRestartResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
jobId: string;
|
|
14
|
+
reused?: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
status: 409 | 429 | 503;
|
|
18
|
+
error: string;
|
|
19
|
+
retryAfterSeconds?: number;
|
|
20
|
+
};
|
|
21
|
+
/** The row for the fleet process itself, as opposed to one of its agents. */
|
|
22
|
+
export declare const APPLY_FLEET_TARGET = "fleet";
|
|
23
|
+
export type ApplyTargetKind = "hot" | "restart";
|
|
24
|
+
/**
|
|
25
|
+
* `restart-required` is a terminal state, not a stalled one: the change is saved
|
|
26
|
+
* and valid, and a running fleet process cannot adopt it. Reporting it as done
|
|
27
|
+
* would tell the user AgEnD is on the new configuration when it is not.
|
|
28
|
+
*/
|
|
29
|
+
export type ApplyTargetStatus = "pending" | "running" | "done" | "failed" | "restart-required";
|
|
30
|
+
export type ApplyJobStatus = "running" | "done" | "failed";
|
|
31
|
+
export interface ApplyTarget {
|
|
32
|
+
/** Instance name, or `fleet` for the process-level row. */
|
|
33
|
+
target: string;
|
|
34
|
+
kind: ApplyTargetKind;
|
|
35
|
+
status: ApplyTargetStatus;
|
|
36
|
+
error?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Who finished the row, when it was not this job's own work:
|
|
39
|
+
* `fleet-restart` — a process restart applied it;
|
|
40
|
+
* `no-change` — the plan expected work the reconcile found unnecessary.
|
|
41
|
+
*/
|
|
42
|
+
settled_by?: "fleet-restart" | "no-change";
|
|
43
|
+
}
|
|
44
|
+
export interface ApplyJob {
|
|
45
|
+
id: string;
|
|
46
|
+
/** The client's idempotency key. */
|
|
47
|
+
key: string;
|
|
48
|
+
status: ApplyJobStatus;
|
|
49
|
+
startedAt: number;
|
|
50
|
+
finishedAt?: number;
|
|
51
|
+
targets: ApplyTarget[];
|
|
52
|
+
/** Deadline for "still working" messaging; it never cancels the work. */
|
|
53
|
+
deadlineMs: number;
|
|
54
|
+
/** The process that created the job, so a restart is detectable. */
|
|
55
|
+
pid: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The idempotency key of the self-restart this job's fleet row was consumed
|
|
59
|
+
* by. Stored on the job rather than in memory because the process it belongs
|
|
60
|
+
* to is about to be replaced — a retry after the restart has to find it.
|
|
61
|
+
*/
|
|
62
|
+
restart_key?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Everything a caller needs to render, computed rather than stored so it stays
|
|
65
|
+
* true while the job is being read long after the last write. */
|
|
66
|
+
export interface ApplyJobView extends ApplyJob {
|
|
67
|
+
elapsed_ms: number;
|
|
68
|
+
/** Past the deadline and still working — say so instead of spinning silently. */
|
|
69
|
+
overdue: boolean;
|
|
70
|
+
/** Human sentence for the overdue case; empty otherwise. */
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
export declare function viewOf(job: ApplyJob, now?: number): ApplyJobView;
|
|
74
|
+
/**
|
|
75
|
+
* The jobs file. Small, rewritten whole on every transition — a settings apply
|
|
76
|
+
* happens at human speed, so the simplest durable thing is the right one.
|
|
77
|
+
*/
|
|
78
|
+
/** Just enough of a logger for the one thing this module has to report. */
|
|
79
|
+
export interface ApplyJobLogger {
|
|
80
|
+
warn(data: unknown, message: string): void;
|
|
81
|
+
}
|
|
82
|
+
export declare class ApplyJobStore {
|
|
83
|
+
private readonly dataDir;
|
|
84
|
+
private readonly now;
|
|
85
|
+
private readonly logger?;
|
|
86
|
+
private jobs;
|
|
87
|
+
constructor(dataDir: string, now?: () => number, logger?: ApplyJobLogger | undefined);
|
|
88
|
+
private path;
|
|
89
|
+
private read;
|
|
90
|
+
private write;
|
|
91
|
+
private prune;
|
|
92
|
+
/**
|
|
93
|
+
* A job created in an earlier process cannot still be running in this one.
|
|
94
|
+
*
|
|
95
|
+
* The restart is what applied the config: every agent came up from the saved
|
|
96
|
+
* file. So rows left pending or running are done — recorded as settled by the
|
|
97
|
+
* restart rather than by the job, because "who finished this" is exactly what
|
|
98
|
+
* the user is trying to find out.
|
|
99
|
+
*/
|
|
100
|
+
settleAfterRestart(): ApplyJob[];
|
|
101
|
+
get(id: string): ApplyJob | null;
|
|
102
|
+
/** The job whose fleet row a given self-restart key already consumed. */
|
|
103
|
+
findByRestartKey(key: string): ApplyJob | null;
|
|
104
|
+
/** The live job for a key, if the client is retrying rather than re-applying. */
|
|
105
|
+
findByKey(key: string): ApplyJob | null;
|
|
106
|
+
create(key: string, targets: Array<Pick<ApplyTarget, "target" | "kind">>, deadlineMs?: number): ApplyJob;
|
|
107
|
+
/** Mutate one job and persist. Every transition hits the disk, because the
|
|
108
|
+
* transition we care most about is the last one before the process dies. */
|
|
109
|
+
update(id: string, mutate: (job: ApplyJob) => void): ApplyJob | null;
|
|
110
|
+
setTargetStatus(id: string, target: string, status: ApplyTargetStatus, error?: string): void;
|
|
111
|
+
finish(id: string, error?: string): void;
|
|
112
|
+
/** Test/introspection seam. */
|
|
113
|
+
all(): ApplyJob[];
|
|
114
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Settings "Apply" that the user can watch, and that survives the restart it
|
|
3
|
+
* asks for.
|
|
4
|
+
*
|
|
5
|
+
* The old path was `POST /api/settings/reload` → SIGHUP → nothing. The page said
|
|
6
|
+
* "changes applied" the moment the signal was sent, while the reconcile was
|
|
7
|
+
* still stopping and starting agents behind it, and a change that restarts AgEnD
|
|
8
|
+
* itself killed the only place the answer lived.
|
|
9
|
+
*
|
|
10
|
+
* Two properties carry the whole design:
|
|
11
|
+
*
|
|
12
|
+
* 1. **The job is on disk, not in the process.** A fleet-impact change takes the
|
|
13
|
+
* fleet process down with it. An in-memory job dies there, `GET` starts
|
|
14
|
+
* answering 404, and the page shows "restarting…" forever (#722/#748 are the
|
|
15
|
+
* same shape). The replacement process reads the marker, settles whatever was
|
|
16
|
+
* still in flight, and can answer for a job it never started.
|
|
17
|
+
* 2. **The client owns the idempotency key.** A server-minted job id cannot stop
|
|
18
|
+
* a duplicate: on a phone the POST leaves, the answer never arrives, and the
|
|
19
|
+
* retry is a second request the server has no way to recognise. A key the
|
|
20
|
+
* client generates before the first attempt makes the retry identifiable, so
|
|
21
|
+
* it returns the original job instead of applying everything twice.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
const FILE = "settings-apply-jobs.json";
|
|
27
|
+
/** How long a finished job stays answerable, and how long a key is honoured. */
|
|
28
|
+
export const APPLY_JOB_RETENTION_MS = 30 * 60_000;
|
|
29
|
+
/**
|
|
30
|
+
* Wall clock, not a step count: the user is looking at a spinner, and the thing
|
|
31
|
+
* they need to be told is that it has been N seconds, not which internal phase
|
|
32
|
+
* it is in. Generous — a reconcile that restarts several agents on a loaded
|
|
33
|
+
* machine is slow but fine.
|
|
34
|
+
*/
|
|
35
|
+
export const APPLY_JOB_DEADLINE_MS = 120_000;
|
|
36
|
+
/** At most this many jobs are kept, newest first. */
|
|
37
|
+
const MAX_JOBS = 20;
|
|
38
|
+
/** The row for the fleet process itself, as opposed to one of its agents. */
|
|
39
|
+
export const APPLY_FLEET_TARGET = "fleet";
|
|
40
|
+
function isTerminal(status) {
|
|
41
|
+
return status === "done" || status === "failed";
|
|
42
|
+
}
|
|
43
|
+
export function viewOf(job, now = Date.now()) {
|
|
44
|
+
const elapsed = Math.max(0, (job.finishedAt ?? now) - job.startedAt);
|
|
45
|
+
const overdue = !isTerminal(job.status) && elapsed > job.deadlineMs;
|
|
46
|
+
return {
|
|
47
|
+
...job,
|
|
48
|
+
elapsed_ms: elapsed,
|
|
49
|
+
overdue,
|
|
50
|
+
message: overdue ? `Still restarting (${Math.round(elapsed / 1000)}s)` : "",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export class ApplyJobStore {
|
|
54
|
+
dataDir;
|
|
55
|
+
now;
|
|
56
|
+
logger;
|
|
57
|
+
jobs = [];
|
|
58
|
+
constructor(dataDir, now = Date.now, logger) {
|
|
59
|
+
this.dataDir = dataDir;
|
|
60
|
+
this.now = now;
|
|
61
|
+
this.logger = logger;
|
|
62
|
+
this.jobs = this.read();
|
|
63
|
+
}
|
|
64
|
+
path() {
|
|
65
|
+
return join(this.dataDir, FILE);
|
|
66
|
+
}
|
|
67
|
+
read() {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(readFileSync(this.path(), "utf-8"));
|
|
70
|
+
if (!Array.isArray(parsed.jobs))
|
|
71
|
+
return [];
|
|
72
|
+
return parsed.jobs.filter(job => typeof job?.id === "string"
|
|
73
|
+
&& typeof job?.key === "string"
|
|
74
|
+
&& typeof job?.startedAt === "number"
|
|
75
|
+
&& Array.isArray(job?.targets));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Missing or corrupt: an unreadable history must not stop the user from
|
|
79
|
+
// applying settings.
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
write() {
|
|
84
|
+
const path = this.path();
|
|
85
|
+
const temp = `${path}.${process.pid}.${this.now()}.tmp`;
|
|
86
|
+
try {
|
|
87
|
+
writeFileSync(temp, JSON.stringify({ jobs: this.jobs }), { mode: 0o600 });
|
|
88
|
+
renameSync(temp, path);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
try {
|
|
92
|
+
unlinkSync(temp);
|
|
93
|
+
}
|
|
94
|
+
catch { /* never created */ }
|
|
95
|
+
// A full or read-only data dir means this job will not survive a restart.
|
|
96
|
+
// Swallowing that silently is how the symptom becomes "progress just
|
|
97
|
+
// vanished" with nothing to look at.
|
|
98
|
+
this.logger?.warn({ err, path }, "Settings apply job could not be persisted");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
prune() {
|
|
102
|
+
const cutoff = this.now() - APPLY_JOB_RETENTION_MS;
|
|
103
|
+
this.jobs = this.jobs
|
|
104
|
+
.filter(job => (job.finishedAt ?? job.startedAt) >= cutoff)
|
|
105
|
+
.slice(0, MAX_JOBS);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A job created in an earlier process cannot still be running in this one.
|
|
109
|
+
*
|
|
110
|
+
* The restart is what applied the config: every agent came up from the saved
|
|
111
|
+
* file. So rows left pending or running are done — recorded as settled by the
|
|
112
|
+
* restart rather than by the job, because "who finished this" is exactly what
|
|
113
|
+
* the user is trying to find out.
|
|
114
|
+
*/
|
|
115
|
+
settleAfterRestart() {
|
|
116
|
+
const settled = [];
|
|
117
|
+
for (const job of this.jobs) {
|
|
118
|
+
if (isTerminal(job.status))
|
|
119
|
+
continue;
|
|
120
|
+
// Only a job from another process. The health server accepts requests a
|
|
121
|
+
// little before startup finishes, so an apply started seconds ago in THIS
|
|
122
|
+
// process would otherwise be declared finished by a restart that already
|
|
123
|
+
// happened. (A recycled pid would leave such a job running until its
|
|
124
|
+
// deadline reports it — the harmless direction.)
|
|
125
|
+
if (job.pid === process.pid)
|
|
126
|
+
continue;
|
|
127
|
+
for (const row of job.targets) {
|
|
128
|
+
if (row.status === "done" || row.status === "failed")
|
|
129
|
+
continue;
|
|
130
|
+
row.status = "done";
|
|
131
|
+
row.settled_by = "fleet-restart";
|
|
132
|
+
}
|
|
133
|
+
job.status = "done";
|
|
134
|
+
job.finishedAt = this.now();
|
|
135
|
+
settled.push(job);
|
|
136
|
+
}
|
|
137
|
+
if (settled.length)
|
|
138
|
+
this.write();
|
|
139
|
+
return settled;
|
|
140
|
+
}
|
|
141
|
+
get(id) {
|
|
142
|
+
return this.jobs.find(job => job.id === id) ?? null;
|
|
143
|
+
}
|
|
144
|
+
/** The job whose fleet row a given self-restart key already consumed. */
|
|
145
|
+
findByRestartKey(key) {
|
|
146
|
+
return this.jobs.find(job => job.restart_key === key) ?? null;
|
|
147
|
+
}
|
|
148
|
+
/** The live job for a key, if the client is retrying rather than re-applying. */
|
|
149
|
+
findByKey(key) {
|
|
150
|
+
const cutoff = this.now() - APPLY_JOB_RETENTION_MS;
|
|
151
|
+
return this.jobs.find(job => job.key === key && (job.finishedAt ?? job.startedAt) >= cutoff) ?? null;
|
|
152
|
+
}
|
|
153
|
+
create(key, targets, deadlineMs = APPLY_JOB_DEADLINE_MS) {
|
|
154
|
+
const job = {
|
|
155
|
+
id: randomUUID(),
|
|
156
|
+
key,
|
|
157
|
+
status: "running",
|
|
158
|
+
startedAt: this.now(),
|
|
159
|
+
targets: targets.map(row => ({ ...row, status: "pending" })),
|
|
160
|
+
deadlineMs,
|
|
161
|
+
pid: process.pid,
|
|
162
|
+
};
|
|
163
|
+
this.jobs.unshift(job);
|
|
164
|
+
this.prune();
|
|
165
|
+
this.write();
|
|
166
|
+
return job;
|
|
167
|
+
}
|
|
168
|
+
/** Mutate one job and persist. Every transition hits the disk, because the
|
|
169
|
+
* transition we care most about is the last one before the process dies. */
|
|
170
|
+
update(id, mutate) {
|
|
171
|
+
const job = this.get(id);
|
|
172
|
+
if (!job)
|
|
173
|
+
return null;
|
|
174
|
+
mutate(job);
|
|
175
|
+
this.write();
|
|
176
|
+
return job;
|
|
177
|
+
}
|
|
178
|
+
setTargetStatus(id, target, status, error) {
|
|
179
|
+
this.update(id, job => {
|
|
180
|
+
const row = job.targets.find(item => item.target === target);
|
|
181
|
+
if (!row)
|
|
182
|
+
return;
|
|
183
|
+
row.status = status;
|
|
184
|
+
if (error)
|
|
185
|
+
row.error = error;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finish(id, error) {
|
|
189
|
+
this.update(id, job => {
|
|
190
|
+
for (const row of job.targets) {
|
|
191
|
+
if (row.status !== "pending" && row.status !== "running")
|
|
192
|
+
continue;
|
|
193
|
+
if (error) {
|
|
194
|
+
row.status = "failed";
|
|
195
|
+
row.error = error;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// The plan forecast work the reconcile then found unnecessary. Saying
|
|
199
|
+
// plain "done" would read as "this agent was restarted".
|
|
200
|
+
row.status = "done";
|
|
201
|
+
row.settled_by = "no-change";
|
|
202
|
+
}
|
|
203
|
+
job.status = error ? "failed" : job.targets.some(row => row.status === "failed") ? "failed" : "done";
|
|
204
|
+
job.finishedAt = this.now();
|
|
205
|
+
if (error)
|
|
206
|
+
job.error = error;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** Test/introspection seam. */
|
|
210
|
+
all() {
|
|
211
|
+
return this.jobs;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=apply-job.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-job.js","sourceRoot":"","sources":["../src/apply-job.ts"],"names":[],"mappings":"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"}
|
package/dist/backend/codex.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare class CodexBackend implements CliBackend {
|
|
|
5
5
|
private binaryPath;
|
|
6
6
|
private readonly sharedCodexHome;
|
|
7
7
|
private readonly isolatedCodexHome;
|
|
8
|
+
/** Which subscription this instance runs on, or null for the shared login. */
|
|
9
|
+
private credentialProfile;
|
|
8
10
|
constructor(instanceDir: string);
|
|
9
11
|
supportsQueuedInput(): boolean;
|
|
10
12
|
/**
|
|
@@ -78,6 +80,16 @@ export declare class CodexBackend implements CliBackend {
|
|
|
78
80
|
* ~/.codex/config.toml (no toml dependency); other settings untouched.
|
|
79
81
|
*/
|
|
80
82
|
private enableContextStatusLine;
|
|
83
|
+
/** Null when the instance did not ask for a profile — today's behaviour. */
|
|
84
|
+
private readProfile;
|
|
85
|
+
/**
|
|
86
|
+
* Where this instance's `auth.json` comes from.
|
|
87
|
+
*
|
|
88
|
+
* The shared home unless a profile says otherwise. A profile owns the file
|
|
89
|
+
* itself — not a copy, not a link — so codex refreshing the token writes
|
|
90
|
+
* into that subscription and no other.
|
|
91
|
+
*/
|
|
92
|
+
private authSourceDir;
|
|
81
93
|
preTrust(workDir: string): void;
|
|
82
94
|
/**
|
|
83
95
|
* Preserve Codex login/session/cache behavior while isolating config.toml.
|
|
@@ -88,6 +100,18 @@ export declare class CodexBackend implements CliBackend {
|
|
|
88
100
|
/** Session dirs every instance must share with the terminal CLI (#506). */
|
|
89
101
|
private static readonly SHARED_SESSION_DIRS;
|
|
90
102
|
private prepareIsolatedHome;
|
|
103
|
+
/**
|
|
104
|
+
* Point this home's `auth.json` at whichever login it should use.
|
|
105
|
+
*
|
|
106
|
+
* Codex refreshes the token by writing through the link rather than replacing
|
|
107
|
+
* it — observed across every instance home on a machine that has been running
|
|
108
|
+
* this arrangement for months, with the shared file's mtime moving. That is
|
|
109
|
+
* behaviour, though, not a contract: a release that starts renaming over the
|
|
110
|
+
* file would leave a real file here and quietly fork the login, with refreshes
|
|
111
|
+
* landing somewhere the profile never sees. So a real file where a link
|
|
112
|
+
* belongs is treated as exactly that — reported, and put back.
|
|
113
|
+
*/
|
|
114
|
+
private linkAuthFile;
|
|
91
115
|
/**
|
|
92
116
|
* One-time heal for an instance whose isolated home grew a REAL session dir
|
|
93
117
|
* (#506): merge it into the shared home without overwriting anything, and
|
package/dist/backend/codex.js
CHANGED
|
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { lastNonBlankRow } from "../pane-input-residue.js";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { probeCliVersion, resolveBinary, shellQuote, validateModel, validateProvider, warnIfModelMismatch } from "./types.js";
|
|
6
|
+
import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
|
|
7
|
+
import { getAgendHome } from "../paths.js";
|
|
6
8
|
import { appendWithMarker, removeMarker } from "./marker-utils.js";
|
|
7
9
|
import { t } from "../locale.js";
|
|
8
10
|
const CODEX_PROJECT_DOC_MAX_BYTES = 32_768;
|
|
@@ -95,6 +97,8 @@ function atomicWritePrivate(path, content) {
|
|
|
95
97
|
}
|
|
96
98
|
// Account-aware models_cache.json is preferred. These documented Codex models
|
|
97
99
|
// are only a last-resort menu when the TUI has not populated its cache yet.
|
|
100
|
+
/** The whole of a codex identity, and the only file a profile owns. */
|
|
101
|
+
const CODEX_AUTH_FILE = "auth.json";
|
|
98
102
|
const CODEX_FALLBACK_MODELS = [
|
|
99
103
|
{ id: "gpt-5.6-sol", label: "GPT-5.6 Sol", description: "frontier agentic coding" },
|
|
100
104
|
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra", description: "balanced agentic coding" },
|
|
@@ -106,6 +110,8 @@ export class CodexBackend {
|
|
|
106
110
|
binaryPath;
|
|
107
111
|
sharedCodexHome;
|
|
108
112
|
isolatedCodexHome;
|
|
113
|
+
/** Which subscription this instance runs on, or null for the shared login. */
|
|
114
|
+
credentialProfile = null;
|
|
109
115
|
constructor(instanceDir) {
|
|
110
116
|
this.instanceDir = instanceDir;
|
|
111
117
|
this.binaryPath = resolveBinary("codex");
|
|
@@ -194,6 +200,7 @@ export class CodexBackend {
|
|
|
194
200
|
}
|
|
195
201
|
buildCommand(config) {
|
|
196
202
|
this.lastKnownModel = config.model?.trim() || null;
|
|
203
|
+
this.credentialProfile = this.readProfile(config);
|
|
197
204
|
const approvalFlag = config.skipPermissions !== false
|
|
198
205
|
? "--dangerously-bypass-approvals-and-sandbox"
|
|
199
206
|
: "--full-auto";
|
|
@@ -227,6 +234,9 @@ export class CodexBackend {
|
|
|
227
234
|
return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
|
|
228
235
|
}
|
|
229
236
|
writeConfig(config) {
|
|
237
|
+
// Set before the home is prepared: which login this instance gets is a
|
|
238
|
+
// property of the home, and the home is built here.
|
|
239
|
+
this.credentialProfile = this.readProfile(config);
|
|
230
240
|
this.prepareIsolatedHome();
|
|
231
241
|
this.cleanSharedConfig();
|
|
232
242
|
// Copy all user settings but no AgEnD MCP entries into a private base
|
|
@@ -356,6 +366,35 @@ export class CodexBackend {
|
|
|
356
366
|
}
|
|
357
367
|
catch { /* best effort — never block launch on statusline config */ }
|
|
358
368
|
}
|
|
369
|
+
/** Null when the instance did not ask for a profile — today's behaviour. */
|
|
370
|
+
readProfile(config) {
|
|
371
|
+
try {
|
|
372
|
+
return resolveCredentialProfile(config.backendOptions);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// A malformed name is refused where it is written; launching is not the
|
|
376
|
+
// place to discover it, and falling back to the shared login is the
|
|
377
|
+
// behaviour every instance had before profiles existed.
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Where this instance's `auth.json` comes from.
|
|
383
|
+
*
|
|
384
|
+
* The shared home unless a profile says otherwise. A profile owns the file
|
|
385
|
+
* itself — not a copy, not a link — so codex refreshing the token writes
|
|
386
|
+
* into that subscription and no other.
|
|
387
|
+
*/
|
|
388
|
+
authSourceDir() {
|
|
389
|
+
if (!this.credentialProfile)
|
|
390
|
+
return this.sharedCodexHome;
|
|
391
|
+
const spec = credentialHomeSpec(this.binaryName);
|
|
392
|
+
if (!spec)
|
|
393
|
+
return this.sharedCodexHome;
|
|
394
|
+
const home = credentialProfileHome(getAgendHome(), this.binaryName, this.credentialProfile);
|
|
395
|
+
prepareCredentialProfileHome(spec, home);
|
|
396
|
+
return home;
|
|
397
|
+
}
|
|
359
398
|
preTrust(workDir) {
|
|
360
399
|
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
361
400
|
let content = "";
|
|
@@ -423,9 +462,16 @@ export class CodexBackend {
|
|
|
423
462
|
if (healedSidecars.length > 0) {
|
|
424
463
|
console.warn(`[agend] removed unsafe Codex SQLite sidecar links: ${healedSidecars.join(", ")}`);
|
|
425
464
|
}
|
|
465
|
+
// One file, possibly from somewhere else. Everything below is unchanged:
|
|
466
|
+
// sessions, the thread/state/memory databases and every cache still come
|
|
467
|
+
// from the shared home, which is why switching subscription does not throw
|
|
468
|
+
// the conversation away.
|
|
469
|
+
this.linkAuthFile();
|
|
426
470
|
for (const name of readdirSync(this.sharedCodexHome)) {
|
|
427
471
|
if (name === "config.toml" || name === AGEND_MCP_CLEANUP_LOCK || name.startsWith(".config.toml."))
|
|
428
472
|
continue;
|
|
473
|
+
if (name === CODEX_AUTH_FILE)
|
|
474
|
+
continue; // handled above, possibly from a profile
|
|
429
475
|
// SQLite resolves a symlinked base DB to the shared path and creates its
|
|
430
476
|
// own adjacent sidecars there. Linking sidecars separately is redundant
|
|
431
477
|
// for shared bases and corrupts the file set for private bases.
|
|
@@ -445,6 +491,62 @@ export class CodexBackend {
|
|
|
445
491
|
}
|
|
446
492
|
}
|
|
447
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Point this home's `auth.json` at whichever login it should use.
|
|
496
|
+
*
|
|
497
|
+
* Codex refreshes the token by writing through the link rather than replacing
|
|
498
|
+
* it — observed across every instance home on a machine that has been running
|
|
499
|
+
* this arrangement for months, with the shared file's mtime moving. That is
|
|
500
|
+
* behaviour, though, not a contract: a release that starts renaming over the
|
|
501
|
+
* file would leave a real file here and quietly fork the login, with refreshes
|
|
502
|
+
* landing somewhere the profile never sees. So a real file where a link
|
|
503
|
+
* belongs is treated as exactly that — reported, and put back.
|
|
504
|
+
*/
|
|
505
|
+
linkAuthFile() {
|
|
506
|
+
const source = join(this.authSourceDir(), CODEX_AUTH_FILE);
|
|
507
|
+
const target = join(this.isolatedCodexHome, CODEX_AUTH_FILE);
|
|
508
|
+
let existing = null;
|
|
509
|
+
try {
|
|
510
|
+
existing = lstatSync(target);
|
|
511
|
+
}
|
|
512
|
+
catch { /* absent */ }
|
|
513
|
+
if (existing?.isSymbolicLink()) {
|
|
514
|
+
let current = "";
|
|
515
|
+
try {
|
|
516
|
+
current = resolve(dirname(target), readlinkSync(target));
|
|
517
|
+
}
|
|
518
|
+
catch { /* unreadable */ }
|
|
519
|
+
if (current === source)
|
|
520
|
+
return;
|
|
521
|
+
try {
|
|
522
|
+
unlinkSync(target);
|
|
523
|
+
}
|
|
524
|
+
catch { /* raced */ }
|
|
525
|
+
}
|
|
526
|
+
else if (existing) {
|
|
527
|
+
// Not a link: codex replaced it, or an older AgEnD copied it. Either way
|
|
528
|
+
// the refreshes it has been collecting are stranded in this instance's
|
|
529
|
+
// home, so say so rather than deleting them silently.
|
|
530
|
+
const stranded = `${target}.replaced-${Date.now()}`;
|
|
531
|
+
try {
|
|
532
|
+
renameSync(target, stranded);
|
|
533
|
+
console.warn(`[agend] codex replaced ${CODEX_AUTH_FILE} in ${this.isolatedCodexHome} with a real file — `
|
|
534
|
+
+ `its login was no longer shared. Kept a copy at ${stranded} and re-linked to ${source}.`);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return; // cannot move it; leave the instance working on what it has
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!existsSync(source))
|
|
541
|
+
return; // not logged in yet; codex will create it
|
|
542
|
+
try {
|
|
543
|
+
symlinkSync(source, target, "file");
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// EEXIST from a concurrent start is the ordinary case and means the link
|
|
547
|
+
// is already there.
|
|
548
|
+
}
|
|
549
|
+
}
|
|
448
550
|
/**
|
|
449
551
|
* One-time heal for an instance whose isolated home grew a REAL session dir
|
|
450
552
|
* (#506): merge it into the shared home without overwriting anything, and
|