codeden 0.2.0 → 0.4.0
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/README.md +8 -2
- package/dist/THIRD-PARTY-NOTICES.txt +26 -0
- package/dist/cli.js +1 -1
- package/dist/drizzle/20261108085346_automation_created_by/migration.sql +1 -0
- package/dist/drizzle/20261108085346_automation_created_by/snapshot.json +3464 -0
- package/dist/drizzle/20261108090252_project_remote_warning/migration.sql +1 -0
- package/dist/drizzle/20261108090252_project_remote_warning/snapshot.json +3474 -0
- package/dist/{main-CzmG0d8t.js → main-B7VuaduN.js} +2 -2
- package/dist/{main-CzmG0d8t.js.map → main-B7VuaduN.js.map} +1 -1
- package/dist/{main-pXwL7GDQ.js → main-sEg3GS7X.js} +21687 -18876
- package/dist/main-sEg3GS7X.js.map +1 -0
- package/dist/{runner-CJetH9jW.js → runner-QPAnitMx.js} +844 -110
- package/dist/runner-QPAnitMx.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/{core-BeOBW-Wr.js → core-1ZGcgEnc.js} +1 -1
- package/dist/web/assets/engine-oniguruma-S10PniCp.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-D-L40tHI.js → highlighted-body-JLMTFACH-Gb9WrOV2.js} +1 -1
- package/dist/web/assets/{highlights-ZCqssNA3.js → highlights-BbG82Nvt.js} +1 -1
- package/dist/web/assets/index-CW3M7yYL.css +1 -0
- package/dist/web/assets/index-Capj1YIA.js +914 -0
- package/dist/web/assets/langs-92p-7kXy.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-D5w0oM_l.js +1 -0
- package/dist/web/assets/{shiki-CWXnlX2q.js → shiki-DvdkI_wn.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-BpK1CZzW.js → stream-tokenizer-hzLEdzf1.js} +1 -1
- package/dist/web/assets/{themes-Bm7B3n10.js → themes-o5fWh38V.js} +2 -2
- package/dist/web/icons/icon-192.png +0 -0
- package/dist/web/icons/icon-512.png +0 -0
- package/dist/web/icons/maskable-192.png +0 -0
- package/dist/web/icons/maskable-512.png +0 -0
- package/dist/web/index.html +10 -2
- package/dist/web/manifest.webmanifest +28 -0
- package/package.json +1 -1
- package/dist/main-pXwL7GDQ.js.map +0 -1
- package/dist/runner-CJetH9jW.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-BLpLufIT.js +0 -1
- package/dist/web/assets/index-D8qaPF_E.css +0 -1
- package/dist/web/assets/index-DR-I5EC5.js +0 -892
- package/dist/web/assets/langs-DKjK0GUN.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-fZTzmfWq.js +0 -1
|
@@ -8,7 +8,7 @@ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypt
|
|
|
8
8
|
import { execFile, spawn } from "node:child_process";
|
|
9
9
|
import { gunzipSync } from "node:zlib";
|
|
10
10
|
import { createInterface } from "node:readline";
|
|
11
|
-
import { copyFile, mkdir, readFile, rm, rmdir, stat, utimes, writeFile } from "node:fs/promises";
|
|
11
|
+
import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
12
12
|
import { STATUS_CODES, ServerResponse, createServer } from "node:http";
|
|
13
13
|
import { Http2ServerRequest, constants as constants$1 } from "node:http2";
|
|
14
14
|
import { Readable } from "node:stream";
|
|
@@ -6004,12 +6004,16 @@ const Role = _enum([
|
|
|
6004
6004
|
"agent"
|
|
6005
6005
|
]);
|
|
6006
6006
|
/**
|
|
6007
|
-
*
|
|
6007
|
+
* Permission levels of an agent session (§11.2), mapped onto each provider's native enforcement.
|
|
6008
|
+
* Shown in the UI as "Permissions"; stored as `autonomy`.
|
|
6009
|
+
* - `read_only`: reads and runs checks, but never edits files, commits or pushes. Needs a
|
|
6010
|
+
* provider with `ProviderCapabilities.readOnly`.
|
|
6008
6011
|
* - `supervised`: every edit and command asks.
|
|
6009
6012
|
* - `auto`: edits and normal commands in the worktree run freely; risky actions ask (default).
|
|
6010
6013
|
* - `full`: no approvals inside the worktree.
|
|
6011
6014
|
*/
|
|
6012
6015
|
const AutonomyLevel = _enum([
|
|
6016
|
+
"read_only",
|
|
6013
6017
|
"supervised",
|
|
6014
6018
|
"auto",
|
|
6015
6019
|
"full"
|
|
@@ -6284,11 +6288,47 @@ const Attachment = discriminatedUnion("type", [object({
|
|
|
6284
6288
|
name: string().optional(),
|
|
6285
6289
|
mediaType: string().optional()
|
|
6286
6290
|
})]);
|
|
6291
|
+
/**
|
|
6292
|
+
* Who sent a message to a session: the user, the orchestrator (to a PM), a PM (to its
|
|
6293
|
+
* sessions), an automation, or CodeDen itself (automatic wake-ups, the factory loop).
|
|
6294
|
+
*/
|
|
6295
|
+
const MessageSender = _enum([
|
|
6296
|
+
"user",
|
|
6297
|
+
"orchestrator",
|
|
6298
|
+
"pm",
|
|
6299
|
+
"automation",
|
|
6300
|
+
"codeden"
|
|
6301
|
+
]);
|
|
6287
6302
|
/** Input that starts or steers a turn: text plus optional image/file attachments. */
|
|
6288
6303
|
const UserInput = object({
|
|
6289
6304
|
text: string(),
|
|
6290
|
-
attachments: array(Attachment).default([])
|
|
6305
|
+
attachments: array(Attachment).default([]),
|
|
6306
|
+
/**
|
|
6307
|
+
* Who wrote the message, when it was not the user: recorded so the transcript can show the
|
|
6308
|
+
* sender without reading the text. Absent on the user's own messages and on every message
|
|
6309
|
+
* stored before senders were recorded.
|
|
6310
|
+
*/
|
|
6311
|
+
from: MessageSender.optional()
|
|
6291
6312
|
});
|
|
6313
|
+
/**
|
|
6314
|
+
* The first line of a message from a PM or the orchestrator, telling the agent who wrote it.
|
|
6315
|
+
* The only definition of these words: the server writes them, the UI recognises them.
|
|
6316
|
+
*/
|
|
6317
|
+
const SENDER_LINES = {
|
|
6318
|
+
pm: "Message from your project manager:",
|
|
6319
|
+
orchestrator: "Message from the orchestrator:"
|
|
6320
|
+
};
|
|
6321
|
+
/**
|
|
6322
|
+
* Input for a message from a PM or the orchestrator: the sender is recorded in `from` and
|
|
6323
|
+
* named for the agent in the text's first line (`SENDER_LINES`).
|
|
6324
|
+
*/
|
|
6325
|
+
function messageFrom(from, text) {
|
|
6326
|
+
return {
|
|
6327
|
+
text: `${SENDER_LINES[from]}\n${text}`,
|
|
6328
|
+
attachments: [],
|
|
6329
|
+
from
|
|
6330
|
+
};
|
|
6331
|
+
}
|
|
6292
6332
|
const itemBase = {
|
|
6293
6333
|
/** Unique within the session. */
|
|
6294
6334
|
id: Id,
|
|
@@ -6813,6 +6853,18 @@ const FactoryLoopState = _enum([
|
|
|
6813
6853
|
"stopped"
|
|
6814
6854
|
]);
|
|
6815
6855
|
// ../../packages/contracts/src/entities/project.ts
|
|
6856
|
+
/**
|
|
6857
|
+
* Something re-detecting a project's folder found that CodeDen does not apply on its own (§9).
|
|
6858
|
+
* `github_remote_missing`: a `github` project's folder no longer has a GitHub `origin` (removed,
|
|
6859
|
+
* pointing at another host, or no git checkout at all). The project keeps its GitHub
|
|
6860
|
+
* coordinates until the user restores the remote or disconnects GitHub.
|
|
6861
|
+
*/
|
|
6862
|
+
const ProjectRemoteWarning = object({
|
|
6863
|
+
code: literal("github_remote_missing"),
|
|
6864
|
+
/** What `origin` points at now, credentials removed; null when there is no `origin`. */
|
|
6865
|
+
remoteUrl: string().nullable(),
|
|
6866
|
+
detectedAt: Timestamp
|
|
6867
|
+
});
|
|
6816
6868
|
const projectBase = {
|
|
6817
6869
|
id: Id,
|
|
6818
6870
|
name: string().min(1),
|
|
@@ -6845,6 +6897,12 @@ const projectBase = {
|
|
|
6845
6897
|
* `projects.reordered`; archived projects keep theirs.
|
|
6846
6898
|
*/
|
|
6847
6899
|
sortOrder: int().nonnegative().default(0),
|
|
6900
|
+
/**
|
|
6901
|
+
* Set when re-detection found the folder's git remote out of step with the project (see
|
|
6902
|
+
* `ProjectRemoteWarning`); absent when all is well. Cleared by the next detection that finds
|
|
6903
|
+
* the GitHub remote again, or by disconnecting GitHub.
|
|
6904
|
+
*/
|
|
6905
|
+
remoteWarning: ProjectRemoteWarning.optional(),
|
|
6848
6906
|
createdAt: Timestamp,
|
|
6849
6907
|
updatedAt: Timestamp,
|
|
6850
6908
|
archivedAt: Timestamp.nullable()
|
|
@@ -6859,6 +6917,9 @@ const GitHubRepo = object({
|
|
|
6859
6917
|
/**
|
|
6860
6918
|
* A project. `github` projects get the full issue/PR/evidence flow; `local` projects
|
|
6861
6919
|
* (no GitHub remote) get sessions, worktrees, checkpoints, diffs and automations only (§9).
|
|
6920
|
+
* The kind is detected when the project is added and again later (on server start, after
|
|
6921
|
+
* agent turns in local projects, on demand): a folder that gains a GitHub `origin` becomes a
|
|
6922
|
+
* `github` project. Losing the remote never downgrades a project on its own.
|
|
6862
6923
|
*/
|
|
6863
6924
|
const Project = discriminatedUnion("kind", [object({
|
|
6864
6925
|
...projectBase,
|
|
@@ -7044,7 +7105,13 @@ const ProviderCapabilities = object({
|
|
|
7044
7105
|
* An autonomy change reaches a running turn from its next tool call. When false (or absent),
|
|
7045
7106
|
* a running turn keeps its autonomy and the change applies from the next turn.
|
|
7046
7107
|
*/
|
|
7047
|
-
liveAutonomy: boolean().optional()
|
|
7108
|
+
liveAutonomy: boolean().optional(),
|
|
7109
|
+
/**
|
|
7110
|
+
* The `read_only` permission level is enforced natively: no file edits, no writing shell
|
|
7111
|
+
* commands, no commits or pushes (§11.2). When false (or absent), sessions on this provider
|
|
7112
|
+
* cannot use `read_only`.
|
|
7113
|
+
*/
|
|
7114
|
+
readOnly: boolean().optional()
|
|
7048
7115
|
});
|
|
7049
7116
|
/** A model a provider offers. */
|
|
7050
7117
|
const ModelInfo = object({
|
|
@@ -7703,7 +7770,11 @@ const RunnerCommandResults = {
|
|
|
7703
7770
|
ok: boolean()
|
|
7704
7771
|
})).optional()
|
|
7705
7772
|
}),
|
|
7706
|
-
"workspace.remove": object({
|
|
7773
|
+
"workspace.remove": object({
|
|
7774
|
+
removed: boolean(),
|
|
7775
|
+
/** A forced removal deleted uncommitted changes (absent from older runners). */
|
|
7776
|
+
discardedChanges: boolean().optional()
|
|
7777
|
+
}),
|
|
7707
7778
|
"session.start": object({ resumeToken: string().nullable() }),
|
|
7708
7779
|
"session.send": Empty,
|
|
7709
7780
|
"session.steer": Empty,
|
|
@@ -7813,13 +7884,7 @@ const QueuedMessage = object({
|
|
|
7813
7884
|
sessionId: Id,
|
|
7814
7885
|
input: UserInput,
|
|
7815
7886
|
/** Who queued it. Events written before senders were recorded were all the user's. */
|
|
7816
|
-
from:
|
|
7817
|
-
"user",
|
|
7818
|
-
"orchestrator",
|
|
7819
|
-
"pm",
|
|
7820
|
-
"automation",
|
|
7821
|
-
"codeden"
|
|
7822
|
-
]).default("user"),
|
|
7887
|
+
from: MessageSender.default("user"),
|
|
7823
7888
|
createdAt: Timestamp
|
|
7824
7889
|
});
|
|
7825
7890
|
object({ messages: array(QueuedMessage) });
|
|
@@ -8171,6 +8236,12 @@ const AutomationGuards = object({
|
|
|
8171
8236
|
/** Run once for missed schedules after downtime. */
|
|
8172
8237
|
catchUpMissed: boolean().default(false)
|
|
8173
8238
|
});
|
|
8239
|
+
/** Who created a database automation (§10). */
|
|
8240
|
+
const AutomationCreator = _enum([
|
|
8241
|
+
"user",
|
|
8242
|
+
"pm",
|
|
8243
|
+
"import"
|
|
8244
|
+
]);
|
|
8174
8245
|
/** An automation: trigger + action + target + guards. Off by default. */
|
|
8175
8246
|
const Automation = object({
|
|
8176
8247
|
id: Id,
|
|
@@ -8196,6 +8267,11 @@ const Automation = object({
|
|
|
8196
8267
|
* `automation:<template id or name slug>`.
|
|
8197
8268
|
*/
|
|
8198
8269
|
workKey: string().min(1).max(200).optional(),
|
|
8270
|
+
/**
|
|
8271
|
+
* Who created a database automation: the user in the UI (`user`, the default when absent),
|
|
8272
|
+
* a PM through `create_automation` (`pm`), or an import of shared files (`import`).
|
|
8273
|
+
*/
|
|
8274
|
+
createdBy: AutomationCreator.optional(),
|
|
8199
8275
|
createdAt: Timestamp,
|
|
8200
8276
|
updatedAt: Timestamp
|
|
8201
8277
|
});
|
|
@@ -9625,7 +9701,7 @@ const Settings = object({
|
|
|
9625
9701
|
/** Stops every project's factory loop and all factory sessions. */
|
|
9626
9702
|
killSwitch: boolean().default(false) }).prefault({}),
|
|
9627
9703
|
/**
|
|
9628
|
-
* Worktree cleanup (§9). Archiving a session
|
|
9704
|
+
* Worktree cleanup (§9). Archiving a session removes its worktree (unless it has uncommitted changes and isn't forced); with `cleanup` on,
|
|
9629
9705
|
* worktrees of sessions inactive for `afterDays` are removed too, unless the session's pull
|
|
9630
9706
|
* request is still open or the worktree has uncommitted changes.
|
|
9631
9707
|
*/
|
|
@@ -9928,20 +10004,22 @@ const CreateProjectRequest = object({
|
|
|
9928
10004
|
defaultProvider: ProviderId.nullable().optional(),
|
|
9929
10005
|
autonomy: AutonomyLevel.nullable().optional()
|
|
9930
10006
|
});
|
|
10007
|
+
/** What CodeDen found in a project folder. */
|
|
10008
|
+
const ProjectDetection = object({
|
|
10009
|
+
isGitRepo: boolean(),
|
|
10010
|
+
/** Top level of the git checkout (may differ from the chosen folder). */
|
|
10011
|
+
gitRoot: string().nullable(),
|
|
10012
|
+
remoteUrl: string().nullable(),
|
|
10013
|
+
github: GitHubRepo.nullable(),
|
|
10014
|
+
defaultBranch: string().nullable(),
|
|
10015
|
+
currentBranch: string().nullable(),
|
|
10016
|
+
packageManager: PackageManager.nullable(),
|
|
10017
|
+
/** Suggested worktree setup commands, e.g. `pnpm install`. */
|
|
10018
|
+
suggestedSetupCommands: array(string())
|
|
10019
|
+
});
|
|
9931
10020
|
object({
|
|
9932
10021
|
project: Project,
|
|
9933
|
-
detected:
|
|
9934
|
-
isGitRepo: boolean(),
|
|
9935
|
-
/** Top level of the git checkout (may differ from the chosen folder). */
|
|
9936
|
-
gitRoot: string().nullable(),
|
|
9937
|
-
remoteUrl: string().nullable(),
|
|
9938
|
-
github: GitHubRepo.nullable(),
|
|
9939
|
-
defaultBranch: string().nullable(),
|
|
9940
|
-
currentBranch: string().nullable(),
|
|
9941
|
-
packageManager: PackageManager.nullable(),
|
|
9942
|
-
/** Suggested worktree setup commands, e.g. `pnpm install`. */
|
|
9943
|
-
suggestedSetupCommands: array(string())
|
|
9944
|
-
})
|
|
10022
|
+
detected: ProjectDetection
|
|
9945
10023
|
});
|
|
9946
10024
|
/** `PATCH /api/projects/:id`. */
|
|
9947
10025
|
const UpdateProjectRequest = object({
|
|
@@ -10540,6 +10618,97 @@ object({
|
|
|
10540
10618
|
/** The session start when accepted (possibly `conflict`); null when declined. */
|
|
10541
10619
|
start: StartSessionResponse.nullable()
|
|
10542
10620
|
});
|
|
10621
|
+
// ../../packages/contracts/src/automations-sharing.ts
|
|
10622
|
+
/**
|
|
10623
|
+
* Sharing automations (§10): duplicate, export as the Markdown + YAML frontmatter file format
|
|
10624
|
+
* (one `.md`, or all of a project's as a `.zip`), and import those files back. Imports are
|
|
10625
|
+
* parsed with the same parser and validation as automation files, never run anything and
|
|
10626
|
+
* always start off.
|
|
10627
|
+
*/
|
|
10628
|
+
/** Size limits for an import, checked by the server before anything is parsed. */
|
|
10629
|
+
const AUTOMATION_IMPORT_LIMITS = {
|
|
10630
|
+
/** Automation files in one import (Markdown files, zip entries or pasted text). */
|
|
10631
|
+
maxFiles: 50,
|
|
10632
|
+
/** One file's text, in UTF-16 code units (about bytes for ASCII). */
|
|
10633
|
+
maxFileChars: 131072,
|
|
10634
|
+
/** All files together, after unzipping. */
|
|
10635
|
+
maxTotalChars: 1048576,
|
|
10636
|
+
/** A zip as uploaded, before base64 (base64 makes it about a third bigger). */
|
|
10637
|
+
maxZipBytes: 1048576,
|
|
10638
|
+
/** Entries a zip may list, `.md` or not (more is refused before inflating anything). */
|
|
10639
|
+
maxZipEntries: 200
|
|
10640
|
+
};
|
|
10641
|
+
/** What to import: Markdown files, a zip of them (base64) and/or pasted Markdown. */
|
|
10642
|
+
const AutomationImportSource = object({
|
|
10643
|
+
files: array(object({
|
|
10644
|
+
/** File name, e.g. `release-notes.md`; its stem becomes the default work key. */
|
|
10645
|
+
name: string().trim().min(1).max(255),
|
|
10646
|
+
content: string().max(AUTOMATION_IMPORT_LIMITS.maxFileChars)
|
|
10647
|
+
})).max(AUTOMATION_IMPORT_LIMITS.maxFiles).optional(),
|
|
10648
|
+
/** A `.zip` of `.md` files, base64-encoded. Other entries are ignored. */
|
|
10649
|
+
zipBase64: string().max(Math.ceil(AUTOMATION_IMPORT_LIMITS.maxZipBytes / 3) * 4 + 4).optional(),
|
|
10650
|
+
/** One automation pasted as text. */
|
|
10651
|
+
text: string().max(AUTOMATION_IMPORT_LIMITS.maxFileChars).optional()
|
|
10652
|
+
}).refine((s) => Boolean(s.files?.length || s.zipBase64 || s.text?.trim()), { message: "Pass files, a zip or pasted text" });
|
|
10653
|
+
/** The definition an import would create (no id, target or switch yet). */
|
|
10654
|
+
const AutomationDraft = object({
|
|
10655
|
+
name: string().min(1),
|
|
10656
|
+
description: string().nullable(),
|
|
10657
|
+
trigger: AutomationTrigger,
|
|
10658
|
+
action: AutomationAction,
|
|
10659
|
+
guards: AutomationGuards,
|
|
10660
|
+
workKey: string().min(1).max(200)
|
|
10661
|
+
});
|
|
10662
|
+
object({ items: array(object({
|
|
10663
|
+
/** File name, unique within the import (duplicates get ` (2)`). */
|
|
10664
|
+
fileName: string(),
|
|
10665
|
+
/** Null when the file is invalid. */
|
|
10666
|
+
automation: AutomationDraft.nullable(),
|
|
10667
|
+
/** Validation error (bad YAML, unknown fields, invalid cron, ...); null when importable. */
|
|
10668
|
+
error: string().nullable(),
|
|
10669
|
+
/** Runs with Full permissions: it never asks before acting. */
|
|
10670
|
+
fullPermissions: boolean()
|
|
10671
|
+
})) });
|
|
10672
|
+
/**
|
|
10673
|
+
* `POST /api/automations/import`: re-parse the same source and create the selected files as
|
|
10674
|
+
* database automations, always off. `projectId` targets one project; omitted, all projects.
|
|
10675
|
+
*/
|
|
10676
|
+
const ImportAutomationsRequest = object({
|
|
10677
|
+
source: AutomationImportSource,
|
|
10678
|
+
projectId: Id.optional(),
|
|
10679
|
+
/** File names (from the preview) to import; omitted, every valid file. */
|
|
10680
|
+
select: array(string()).max(AUTOMATION_IMPORT_LIMITS.maxFiles).optional()
|
|
10681
|
+
});
|
|
10682
|
+
object({
|
|
10683
|
+
/** The new automations, all off. */
|
|
10684
|
+
created: array(AutomationListItem),
|
|
10685
|
+
/** Selected files that could not be imported, with the reason. */
|
|
10686
|
+
failed: array(object({
|
|
10687
|
+
fileName: string(),
|
|
10688
|
+
error: string()
|
|
10689
|
+
}))
|
|
10690
|
+
});
|
|
10691
|
+
object({
|
|
10692
|
+
/** `<slug>.md`. */
|
|
10693
|
+
fileName: string().min(1),
|
|
10694
|
+
markdown: string()
|
|
10695
|
+
});
|
|
10696
|
+
/** `GET /api/automations/export?projectId=`: the query; without a project, every automation. */
|
|
10697
|
+
const ExportAutomationsQuery = object({ projectId: Id.optional() });
|
|
10698
|
+
object({
|
|
10699
|
+
fileName: string().min(1),
|
|
10700
|
+
/** The zip, base64-encoded. */
|
|
10701
|
+
zipBase64: string(),
|
|
10702
|
+
count: int().nonnegative()
|
|
10703
|
+
});
|
|
10704
|
+
/**
|
|
10705
|
+
* `POST /api/automations/:id/duplicate`: an editable copy (also of file-based and built-in
|
|
10706
|
+
* automations), off. `projectId` targets one project; omitted, the original's target.
|
|
10707
|
+
*/
|
|
10708
|
+
const DuplicateAutomationRequest = object({
|
|
10709
|
+
projectId: Id.optional(),
|
|
10710
|
+
name: string().trim().min(1).max(200).optional()
|
|
10711
|
+
});
|
|
10543
10712
|
// ../../packages/contracts/src/browser-api.ts
|
|
10544
10713
|
/**
|
|
10545
10714
|
* Browser panel HTTP and WebSocket shapes (§12.2). Routes live under
|
|
@@ -10841,6 +11010,201 @@ object({
|
|
|
10841
11010
|
message: string()
|
|
10842
11011
|
}).optional()
|
|
10843
11012
|
});
|
|
11013
|
+
/** The password rules in plain words, shown next to the password field. */
|
|
11014
|
+
const PASSWORD_RULE_TEXT = "Use at least 15 characters. A short sentence or a few random words works well. Very common passwords, repeated characters and simple sequences such as abcdef… or 12345… are refused.";
|
|
11015
|
+
/** Base words of very common passwords; a password made of one of these plus digits is refused. */
|
|
11016
|
+
const COMMON_WORDS = /* @__PURE__ */ new Set([
|
|
11017
|
+
"password",
|
|
11018
|
+
"passw0rd",
|
|
11019
|
+
"letmein",
|
|
11020
|
+
"welcome",
|
|
11021
|
+
"admin",
|
|
11022
|
+
"administrator",
|
|
11023
|
+
"iloveyou",
|
|
11024
|
+
"qwerty",
|
|
11025
|
+
"monkey",
|
|
11026
|
+
"dragon",
|
|
11027
|
+
"football",
|
|
11028
|
+
"baseball",
|
|
11029
|
+
"sunshine",
|
|
11030
|
+
"princess",
|
|
11031
|
+
"superman",
|
|
11032
|
+
"batman",
|
|
11033
|
+
"trustno",
|
|
11034
|
+
"master",
|
|
11035
|
+
"shadow",
|
|
11036
|
+
"michael",
|
|
11037
|
+
"jennifer",
|
|
11038
|
+
"computer",
|
|
11039
|
+
"secret",
|
|
11040
|
+
"changeme",
|
|
11041
|
+
"codeden",
|
|
11042
|
+
"default",
|
|
11043
|
+
"login",
|
|
11044
|
+
"hello",
|
|
11045
|
+
"abc",
|
|
11046
|
+
"test",
|
|
11047
|
+
"guest",
|
|
11048
|
+
"root",
|
|
11049
|
+
"starwars",
|
|
11050
|
+
"whatever",
|
|
11051
|
+
"freedom",
|
|
11052
|
+
"mustang",
|
|
11053
|
+
"pokemon",
|
|
11054
|
+
"charlie",
|
|
11055
|
+
"correcthorsebatterystaple"
|
|
11056
|
+
]);
|
|
11057
|
+
/** Character runs a "sequence" password walks along (alphabet, digits, keyboard rows). */
|
|
11058
|
+
const SEQUENCES = [
|
|
11059
|
+
"abcdefghijklmnopqrstuvwxyz",
|
|
11060
|
+
"0123456789",
|
|
11061
|
+
"qwertyuiopasdfghjklzxcvbnm",
|
|
11062
|
+
"qwertzuiopasdfghjklyxcvbnm",
|
|
11063
|
+
"azertyuiopqsdfghjklmwxcvbn",
|
|
11064
|
+
"1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p",
|
|
11065
|
+
"qazwsxedcrfvtgbyhnujmikolp",
|
|
11066
|
+
"1234567890qwertyuiopasdfghjklzxcvbnm"
|
|
11067
|
+
];
|
|
11068
|
+
/** True when `text` is one short unit repeated (`abcabcabc`, `passwordpassword`). */
|
|
11069
|
+
function isRepeatedUnit(text) {
|
|
11070
|
+
for (let size = 1; size <= text.length / 2; size++) {
|
|
11071
|
+
if (text.length % size !== 0) continue;
|
|
11072
|
+
if (text.slice(0, size).repeat(text.length / size) === text) return true;
|
|
11073
|
+
}
|
|
11074
|
+
return false;
|
|
11075
|
+
}
|
|
11076
|
+
/** True when `text` walks along one of the known sequences, forwards or backwards (wrapping). */
|
|
11077
|
+
function isSequence(text) {
|
|
11078
|
+
return SEQUENCES.some((seq) => {
|
|
11079
|
+
const looped = seq.repeat(Math.ceil(text.length / seq.length) + 2);
|
|
11080
|
+
const reversed = [...looped].reverse().join("");
|
|
11081
|
+
return looped.includes(text) || reversed.includes(text);
|
|
11082
|
+
});
|
|
11083
|
+
}
|
|
11084
|
+
/**
|
|
11085
|
+
* Why `password` can't be used for network access, in plain words, or null when it is fine.
|
|
11086
|
+
* Shared by the server (which enforces it) and the UI (which explains it as you type).
|
|
11087
|
+
*/
|
|
11088
|
+
function passwordProblem(password) {
|
|
11089
|
+
if (password.length < 15) return `Use at least 15 characters (this one has ${password.length}).`;
|
|
11090
|
+
if (password.length > 256) return `Use at most 256 characters.`;
|
|
11091
|
+
const lower = password.toLowerCase();
|
|
11092
|
+
const compact = lower.replace(/\s+/g, "");
|
|
11093
|
+
if (new Set(compact).size < 5 || isRepeatedUnit(compact)) return "That repeats the same few characters. Try a few random words instead.";
|
|
11094
|
+
if (isSequence(compact)) return "That is a simple sequence. Try a few random words instead.";
|
|
11095
|
+
const letters = lower.replace(/[^a-z]/g, "");
|
|
11096
|
+
const words = lower.split(/[^a-z]+/).filter(Boolean);
|
|
11097
|
+
if (letters.length > 0 && (COMMON_WORDS.has(letters) || isRepeatedUnitOf(letters, COMMON_WORDS) || words.length > 0 && words.every((w) => COMMON_WORDS.has(w)))) return "That is too close to a very common password. Try a few random words instead.";
|
|
11098
|
+
return null;
|
|
11099
|
+
}
|
|
11100
|
+
function isRepeatedUnitOf(text, units) {
|
|
11101
|
+
for (const unit of units) if (text.length % unit.length === 0 && unit.repeat(text.length / unit.length) === text) return true;
|
|
11102
|
+
return false;
|
|
11103
|
+
}
|
|
11104
|
+
/** One address CodeDen can be opened on, with the URL a phone would scan. */
|
|
11105
|
+
const NetworkAddress = object({
|
|
11106
|
+
kind: _enum([
|
|
11107
|
+
"local",
|
|
11108
|
+
"lan",
|
|
11109
|
+
"vpn"
|
|
11110
|
+
]),
|
|
11111
|
+
/** Human label, e.g. "This computer", "Wi-Fi (192.168.1.20)", "Tailscale". */
|
|
11112
|
+
label: string(),
|
|
11113
|
+
/** The interface the address belongs to (`Wi-Fi`, `eth0`, `tailscale0`), or null for localhost. */
|
|
11114
|
+
interfaceName: string().nullable(),
|
|
11115
|
+
/** The bare host: `localhost` or an IPv4 address. */
|
|
11116
|
+
host: string(),
|
|
11117
|
+
/** `http://<host>:<port>`. */
|
|
11118
|
+
url: string()
|
|
11119
|
+
});
|
|
11120
|
+
/** A signed-in remote device, as listed in Settings › Network access (never the token). */
|
|
11121
|
+
const RemoteDevice = object({
|
|
11122
|
+
id: string(),
|
|
11123
|
+
/** The browser's user agent when it signed in (trimmed). */
|
|
11124
|
+
label: string(),
|
|
11125
|
+
/** The address it signed in from. */
|
|
11126
|
+
address: string().nullable(),
|
|
11127
|
+
createdAt: Timestamp,
|
|
11128
|
+
lastSeenAt: Timestamp,
|
|
11129
|
+
expiresAt: Timestamp,
|
|
11130
|
+
/** The device making this request. */
|
|
11131
|
+
current: boolean()
|
|
11132
|
+
});
|
|
11133
|
+
/**
|
|
11134
|
+
* `GET /api/network`: whether network access is on, where CodeDen answers, and who is signed
|
|
11135
|
+
* in. `remote` tells the caller whether it is itself a remote device (management is
|
|
11136
|
+
* loopback-only, so the UI shows the controls read-only there).
|
|
11137
|
+
*/
|
|
11138
|
+
const NetworkStatus = object({
|
|
11139
|
+
enabled: boolean(),
|
|
11140
|
+
passwordSet: boolean(),
|
|
11141
|
+
passwordSetAt: Timestamp.nullable(),
|
|
11142
|
+
/** Where the HTTP listener is bound right now. */
|
|
11143
|
+
listening: _enum(["loopback", "network"]),
|
|
11144
|
+
port: int().nonnegative(),
|
|
11145
|
+
/** Addresses CodeDen answers on: localhost always, LAN and VPN addresses when enabled. */
|
|
11146
|
+
addresses: array(NetworkAddress),
|
|
11147
|
+
/** Addresses it would answer on once enabled (for the off state's preview). */
|
|
11148
|
+
candidates: array(NetworkAddress),
|
|
11149
|
+
devices: array(RemoteDevice),
|
|
11150
|
+
remote: boolean()
|
|
11151
|
+
});
|
|
11152
|
+
/**
|
|
11153
|
+
* `POST /api/network/password` (loopback only): set a password, or let the server generate a
|
|
11154
|
+
* passphrase. Either way every remote device is signed out.
|
|
11155
|
+
*/
|
|
11156
|
+
const SetNetworkPasswordRequest = union([object({ password: string().min(1).max(256) }), object({ generate: literal(true) })]);
|
|
11157
|
+
/** Response to setting the password: the generated passphrase is returned once, never again. */
|
|
11158
|
+
const SetNetworkPasswordResponse = object({
|
|
11159
|
+
status: NetworkStatus,
|
|
11160
|
+
generated: string().nullable()
|
|
11161
|
+
});
|
|
11162
|
+
/** `PUT /api/network` (loopback only): turn network access on or off. */
|
|
11163
|
+
const SetNetworkAccessRequest = object({ enabled: boolean() });
|
|
11164
|
+
/** `POST /api/auth/sign-in` from a remote device. */
|
|
11165
|
+
const SignInRequest = object({ password: string().max(256) });
|
|
11166
|
+
object({
|
|
11167
|
+
/** The caller is not on this machine. */
|
|
11168
|
+
remote: boolean(),
|
|
11169
|
+
/** The caller has a valid session (remote) or install-token cookie (local). */
|
|
11170
|
+
signedIn: boolean(),
|
|
11171
|
+
networkEnabled: boolean()
|
|
11172
|
+
});
|
|
11173
|
+
object({ error: object({
|
|
11174
|
+
code: _enum([
|
|
11175
|
+
"invalid_password",
|
|
11176
|
+
"rate_limited",
|
|
11177
|
+
"locked_out",
|
|
11178
|
+
"network_off"
|
|
11179
|
+
]),
|
|
11180
|
+
message: string(),
|
|
11181
|
+
retryAfterSec: int().nonnegative().nullable()
|
|
11182
|
+
}) });
|
|
11183
|
+
/** A stored remote session: only the SHA-256 hash of its token is kept. */
|
|
11184
|
+
const RemoteSessionRecord = object({
|
|
11185
|
+
id: string(),
|
|
11186
|
+
tokenHash: string().regex(/^[0-9a-f]{64}$/),
|
|
11187
|
+
label: string(),
|
|
11188
|
+
address: string().nullable(),
|
|
11189
|
+
createdAt: Timestamp,
|
|
11190
|
+
lastSeenAt: Timestamp,
|
|
11191
|
+
expiresAt: Timestamp
|
|
11192
|
+
});
|
|
11193
|
+
/**
|
|
11194
|
+
* `<home>/remote-access.json` (mode 0600): the network switch, the password hash (scrypt,
|
|
11195
|
+
* salted; never the password) and the remote sessions. Kept out of `settings.json` so the
|
|
11196
|
+
* settings API can't switch network access on without the checks.
|
|
11197
|
+
*/
|
|
11198
|
+
const RemoteAccessFile = object({
|
|
11199
|
+
schemaVersion: literal(1).default(1),
|
|
11200
|
+
enabled: boolean().default(false),
|
|
11201
|
+
password: object({
|
|
11202
|
+
/** `scrypt$<N>$<r>$<p>$<salt b64>$<hash b64>`. */
|
|
11203
|
+
hash: string().startsWith("scrypt$"),
|
|
11204
|
+
setAt: Timestamp
|
|
11205
|
+
}).nullable().default(null),
|
|
11206
|
+
sessions: array(RemoteSessionRecord).default([])
|
|
11207
|
+
});
|
|
10844
11208
|
object({ session: Session.nullable() });
|
|
10845
11209
|
/** `POST .../orchestrator/messages` and `.../projects/:id/pm/messages`. */
|
|
10846
11210
|
const RoleMessageRequest = object({
|
|
@@ -10911,6 +11275,25 @@ object({
|
|
|
10911
11275
|
message: string().nullable()
|
|
10912
11276
|
})
|
|
10913
11277
|
});
|
|
11278
|
+
object({
|
|
11279
|
+
/** The project after the check (unchanged unless `changed`). */
|
|
11280
|
+
project: Project,
|
|
11281
|
+
outcome: _enum([
|
|
11282
|
+
"unchanged",
|
|
11283
|
+
"git_detected",
|
|
11284
|
+
"github_connected",
|
|
11285
|
+
"github_changed",
|
|
11286
|
+
"github_remote_missing",
|
|
11287
|
+
"folder_missing"
|
|
11288
|
+
]),
|
|
11289
|
+
/** True when the check updated the project (a `project.updated` event was appended). */
|
|
11290
|
+
changed: boolean(),
|
|
11291
|
+
/** One line for the UI, e.g. "Connected to octo/app" or "Still no GitHub remote on origin". */
|
|
11292
|
+
message: string(),
|
|
11293
|
+
/** What was found in the folder; null when the folder is missing. */
|
|
11294
|
+
detected: ProjectDetection.nullable()
|
|
11295
|
+
});
|
|
11296
|
+
object({ project: Project });
|
|
10914
11297
|
// ../../packages/contracts/src/runner/events.ts
|
|
10915
11298
|
/**
|
|
10916
11299
|
* Durable runner -> server events. Journaled by the runner and delivered with a `seq`
|
|
@@ -11439,7 +11822,7 @@ object({
|
|
|
11439
11822
|
* rest of the bundle, so the package version is the single source of truth at runtime;
|
|
11440
11823
|
* everywhere else (workspace, tests) it is `SOURCE_VERSION`.
|
|
11441
11824
|
*/
|
|
11442
|
-
const CODEDEN_VERSION = packageVersion() ?? "0.
|
|
11825
|
+
const CODEDEN_VERSION = packageVersion() ?? "0.4.0";
|
|
11443
11826
|
function packageVersion() {
|
|
11444
11827
|
const value = globalThis.__CODEDEN_PACKAGE_VERSION__;
|
|
11445
11828
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -15593,9 +15976,11 @@ function explicitModel(model) {
|
|
|
15593
15976
|
}
|
|
15594
15977
|
// ../../packages/providers/src/policy/read-only.ts
|
|
15595
15978
|
/**
|
|
15596
|
-
* Read-only sessions (the orchestrator and PMs,
|
|
15597
|
-
*
|
|
15598
|
-
* read, with no output redirection and no
|
|
15979
|
+
* Read-only sessions (the orchestrator and PMs, and agent sessions with the `read_only`
|
|
15980
|
+
* permission level, §11.2) may inspect a repository but never change it. This allowlist decides
|
|
15981
|
+
* which shell commands they may run: commands that only read, with no output redirection and no
|
|
15982
|
+
* command substitution. Agent sessions may also run the project's checks (tests, lint, type
|
|
15983
|
+
* checks) without fix or update flags. Anything else is denied.
|
|
15599
15984
|
*/
|
|
15600
15985
|
/**
|
|
15601
15986
|
* Split on every `;`, `&&`, `||`, `|` and newline, even inside quotes. Deliberately cruder
|
|
@@ -15705,8 +16090,75 @@ const READ_ONLY_GH = /* @__PURE__ */ new Set([
|
|
|
15705
16090
|
"checks"
|
|
15706
16091
|
]);
|
|
15707
16092
|
const bin = (token) => (token ?? "").replace(/\\/g, "/").split("/").pop()?.toLowerCase().replace(/\.(exe|cmd|bat|ps1)$/, "") ?? "";
|
|
16093
|
+
/** Package managers whose `test`, `lint`, ... scripts count as checks. */
|
|
16094
|
+
const PACKAGE_MANAGERS = /* @__PURE__ */ new Set([
|
|
16095
|
+
"npm",
|
|
16096
|
+
"pnpm",
|
|
16097
|
+
"yarn",
|
|
16098
|
+
"bun"
|
|
16099
|
+
]);
|
|
16100
|
+
/** Script names a package manager may run as a check (`pnpm test`, `npm run lint`). */
|
|
16101
|
+
const CHECK_SCRIPTS = /* @__PURE__ */ new Set([
|
|
16102
|
+
"test",
|
|
16103
|
+
"tests",
|
|
16104
|
+
"lint",
|
|
16105
|
+
"typecheck",
|
|
16106
|
+
"type-check",
|
|
16107
|
+
"check",
|
|
16108
|
+
"types",
|
|
16109
|
+
"vitest",
|
|
16110
|
+
"jest",
|
|
16111
|
+
"biome",
|
|
16112
|
+
"eslint",
|
|
16113
|
+
"tsc"
|
|
16114
|
+
]);
|
|
16115
|
+
/** Programs (null: any arguments) and `<program> <subcommand>` pairs that run checks. */
|
|
16116
|
+
const CHECK_COMMANDS = {
|
|
16117
|
+
tsc: null,
|
|
16118
|
+
vitest: null,
|
|
16119
|
+
jest: null,
|
|
16120
|
+
eslint: null,
|
|
16121
|
+
pytest: null,
|
|
16122
|
+
mypy: null,
|
|
16123
|
+
pyright: null,
|
|
16124
|
+
biome: [
|
|
16125
|
+
"check",
|
|
16126
|
+
"lint",
|
|
16127
|
+
"ci"
|
|
16128
|
+
],
|
|
16129
|
+
ruff: ["check"],
|
|
16130
|
+
cargo: [
|
|
16131
|
+
"test",
|
|
16132
|
+
"check",
|
|
16133
|
+
"clippy"
|
|
16134
|
+
],
|
|
16135
|
+
go: ["test", "vet"],
|
|
16136
|
+
dotnet: ["test"],
|
|
16137
|
+
mvn: ["test", "verify"],
|
|
16138
|
+
gradle: ["test", "check"]
|
|
16139
|
+
};
|
|
16140
|
+
/** Flags that turn a check into a write (fixing, formatting, updating snapshots, watching). */
|
|
16141
|
+
const WRITING_FLAG = /^(--fix|--fix-type=.*|--write|--apply|--apply-unsafe|--unsafe|-u|--update|--update-snapshots?|--updatesnapshot|--watch|-w)(=.*)?$/;
|
|
16142
|
+
/** True when `name args` runs a known check without writing flags. */
|
|
16143
|
+
function isCheck(name, args) {
|
|
16144
|
+
if (args.some((a) => WRITING_FLAG.test(a))) return false;
|
|
16145
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
16146
|
+
if (PACKAGE_MANAGERS.has(name)) {
|
|
16147
|
+
const script = positional[0] === "run" ? positional[1] : positional[0];
|
|
16148
|
+
return script !== void 0 && CHECK_SCRIPTS.has(script);
|
|
16149
|
+
}
|
|
16150
|
+
if (name === "npx" || name === "pnpx" || name === "bunx") {
|
|
16151
|
+
const tool = positional[0];
|
|
16152
|
+
return tool !== void 0 && isCheck(bin(tool), args.slice(args.indexOf(tool) + 1));
|
|
16153
|
+
}
|
|
16154
|
+
if (name === "python" || name === "python3" || name === "py") return args[0] === "-m" && (args[1] === "pytest" || args[1] === "mypy");
|
|
16155
|
+
if (!Object.hasOwn(CHECK_COMMANDS, name)) return false;
|
|
16156
|
+
const subs = CHECK_COMMANDS[name];
|
|
16157
|
+
if (!subs) return true;
|
|
16158
|
+
return positional[0] !== void 0 && subs.includes(positional[0]);
|
|
16159
|
+
}
|
|
15708
16160
|
/** Why a command is refused in a read-only session, or undefined when it may run. */
|
|
15709
|
-
function readOnlyCommandDenial(command) {
|
|
16161
|
+
function readOnlyCommandDenial(command, options = {}) {
|
|
15710
16162
|
if (/\$\(|`|<\(|>\(/.test(command)) return "Command substitution is not allowed in a read-only session";
|
|
15711
16163
|
for (const simple of splitCommands(command)) {
|
|
15712
16164
|
if (/(^|[^0-9&<])>>?(?!\s*(&\d|\/dev\/null|nul\b|\$null))/i.test(simple)) return "Writing files is not allowed in a read-only session";
|
|
@@ -15725,6 +16177,7 @@ function readOnlyCommandDenial(command) {
|
|
|
15725
16177
|
continue;
|
|
15726
16178
|
}
|
|
15727
16179
|
if (name === "find" && args.some((a) => a === "-delete" || a === "-exec" || a === "-execdir")) return "find with -delete or -exec is not allowed in a read-only session";
|
|
16180
|
+
if (options.allowChecks && isCheck(name, args)) continue;
|
|
15728
16181
|
if (!READ_ONLY_BINS.has(name)) return `${name || "This command"} is not on the read-only allowlist`;
|
|
15729
16182
|
}
|
|
15730
16183
|
}
|
|
@@ -15866,7 +16319,7 @@ function tempRootsFrom(candidates, platform, home) {
|
|
|
15866
16319
|
}
|
|
15867
16320
|
return out;
|
|
15868
16321
|
}
|
|
15869
|
-
const realpath = (p) => {
|
|
16322
|
+
const realpath$1 = (p) => {
|
|
15870
16323
|
try {
|
|
15871
16324
|
return realpathSync.native(p);
|
|
15872
16325
|
} catch {
|
|
@@ -15883,7 +16336,7 @@ function osTempRoots(platform = process.platform) {
|
|
|
15883
16336
|
if (platform !== process.platform) return [];
|
|
15884
16337
|
if (cachedTempRoots) return cachedTempRoots;
|
|
15885
16338
|
const env = process.env;
|
|
15886
|
-
cachedTempRoots = tempRootsFrom([os.tmpdir(), ...platform === "win32" ? [env.TEMP, env.TMP] : [env.TMPDIR, "/tmp"]].flatMap((c) => c ? [c, realpath(c)] : []), platform, os.homedir());
|
|
16339
|
+
cachedTempRoots = tempRootsFrom([os.tmpdir(), ...platform === "win32" ? [env.TEMP, env.TMP] : [env.TMPDIR, "/tmp"]].flatMap((c) => c ? [c, realpath$1(c)] : []), platform, os.homedir());
|
|
15887
16340
|
return cachedTempRoots;
|
|
15888
16341
|
}
|
|
15889
16342
|
const SECRET_BASENAMES = /* @__PURE__ */ new Set([
|
|
@@ -16739,6 +17192,8 @@ function approvalTitle(name, input) {
|
|
|
16739
17192
|
// ../../packages/providers/src/claude/permissions.ts
|
|
16740
17193
|
/**
|
|
16741
17194
|
* Native permission mode per autonomy level (§11.2):
|
|
17195
|
+
* - read_only -> `default`; edits and writing commands are denied by the PreToolUse hook
|
|
17196
|
+
* (`ClaudePolicy.denial`), so they never reach a prompt
|
|
16742
17197
|
* - supervised -> `default`, and every edit/command is forced to ask via a PreToolUse hook
|
|
16743
17198
|
* - auto -> `acceptEdits`, plus the risky-action classifier
|
|
16744
17199
|
* - full -> `bypassPermissions`, asking only for writes outside the worktree (the OS
|
|
@@ -16747,7 +17202,7 @@ function approvalTitle(name, input) {
|
|
|
16747
17202
|
*/
|
|
16748
17203
|
function permissionModeFor(autonomy, planMode) {
|
|
16749
17204
|
if (planMode) return "plan";
|
|
16750
|
-
if (autonomy === "supervised") return "default";
|
|
17205
|
+
if (autonomy === "read_only" || autonomy === "supervised") return "default";
|
|
16751
17206
|
if (autonomy === "auto") return "acceptEdits";
|
|
16752
17207
|
return "bypassPermissions";
|
|
16753
17208
|
}
|
|
@@ -16769,16 +17224,23 @@ var ClaudePolicy = class {
|
|
|
16769
17224
|
get level() {
|
|
16770
17225
|
return this.autonomy;
|
|
16771
17226
|
}
|
|
17227
|
+
/**
|
|
17228
|
+
* True while edits and writing commands are refused: a read-only role (orchestrator, PMs),
|
|
17229
|
+
* or the `read_only` permission level, which can change live with `setAutonomy`.
|
|
17230
|
+
*/
|
|
17231
|
+
get isReadOnly() {
|
|
17232
|
+
return this.readOnly || this.autonomy === "read_only";
|
|
17233
|
+
}
|
|
16772
17234
|
/** Switch the level; the next `gate()` uses it. `allow_session` decisions are kept. */
|
|
16773
17235
|
setAutonomy(level) {
|
|
16774
17236
|
this.autonomy = level;
|
|
16775
17237
|
}
|
|
16776
17238
|
/** Why a call is refused outright (read-only sessions), or undefined. */
|
|
16777
17239
|
denial(toolName, input) {
|
|
16778
|
-
if (!this.
|
|
16779
|
-
if (EDIT_TOOLS.has(toolName)) return "This session is read-only: it cannot edit files. Act through CodeDen tools instead.";
|
|
17240
|
+
if (!this.isReadOnly) return void 0;
|
|
17241
|
+
if (EDIT_TOOLS.has(toolName)) return this.readOnly ? "This session is read-only: it cannot edit files. Act through CodeDen tools instead." : "This session has read-only permissions: it cannot edit files, commit or push. Report what you found instead.";
|
|
16780
17242
|
if (COMMAND_TOOLS.has(toolName)) {
|
|
16781
|
-
const reason = readOnlyCommandDenial(str(input, "command") ?? "");
|
|
17243
|
+
const reason = readOnlyCommandDenial(str(input, "command") ?? "", { allowChecks: !this.readOnly });
|
|
16782
17244
|
return reason ? `This session is read-only: ${reason}.` : void 0;
|
|
16783
17245
|
}
|
|
16784
17246
|
}
|
|
@@ -18649,7 +19111,8 @@ const capabilities$2 = {
|
|
|
18649
19111
|
reasoningEffort: true,
|
|
18650
19112
|
mcp: true,
|
|
18651
19113
|
skills: true,
|
|
18652
|
-
liveAutonomy: true
|
|
19114
|
+
liveAutonomy: true,
|
|
19115
|
+
readOnly: true
|
|
18653
19116
|
};
|
|
18654
19117
|
const EFFORTS = [
|
|
18655
19118
|
"low",
|
|
@@ -19302,6 +19765,7 @@ async function handleServerRequest(req, ac) {
|
|
|
19302
19765
|
// ../../packages/providers/src/codex/config.ts
|
|
19303
19766
|
/**
|
|
19304
19767
|
* Native sandbox + approval policy per autonomy level (§11.2):
|
|
19768
|
+
* - read_only -> read-only sandbox + `never`: writes fail instead of asking, no network
|
|
19305
19769
|
* - supervised -> `untrusted`: every command that isn't known-safe and every patch asks
|
|
19306
19770
|
* - auto -> workspace-write sandbox + `on-request`: the sandbox confines writes (to the
|
|
19307
19771
|
* worktree, extra roots and the OS temp directory) and network; escaping it
|
|
@@ -19310,7 +19774,7 @@ async function handleServerRequest(req, ac) {
|
|
|
19310
19774
|
* to the worktree, extra roots and temp
|
|
19311
19775
|
*/
|
|
19312
19776
|
function codexPolicy(autonomy, readOnly = false) {
|
|
19313
|
-
if (readOnly) return {
|
|
19777
|
+
if (readOnly || autonomy === "read_only") return {
|
|
19314
19778
|
approvalPolicy: "never",
|
|
19315
19779
|
sandbox: "read-only",
|
|
19316
19780
|
networkAccess: false
|
|
@@ -19337,7 +19801,7 @@ function codexPolicy(autonomy, readOnly = false) {
|
|
|
19337
19801
|
* never needs an approval (§11.2).
|
|
19338
19802
|
*/
|
|
19339
19803
|
function codexWritableRoots(opts, tempRoots = osTempRoots()) {
|
|
19340
|
-
const temp = opts.readOnly || opts.autonomy === "supervised" ? [] : tempRoots;
|
|
19804
|
+
const temp = opts.readOnly || opts.autonomy === "read_only" || opts.autonomy === "supervised" ? [] : tempRoots;
|
|
19341
19805
|
return [.../* @__PURE__ */ new Set([...opts.additionalDirectories, ...temp])];
|
|
19342
19806
|
}
|
|
19343
19807
|
/**
|
|
@@ -20309,7 +20773,8 @@ const capabilities$1 = {
|
|
|
20309
20773
|
reasoningEffort: true,
|
|
20310
20774
|
mcp: true,
|
|
20311
20775
|
skills: true,
|
|
20312
|
-
liveAutonomy: false
|
|
20776
|
+
liveAutonomy: false,
|
|
20777
|
+
readOnly: true
|
|
20313
20778
|
};
|
|
20314
20779
|
const CONFIGURED_MODEL = "The model configured in ~/.codex/config.toml";
|
|
20315
20780
|
/**
|
|
@@ -20582,7 +21047,8 @@ var FakeSession = class {
|
|
|
20582
21047
|
await this.streamAssistant(turn, threadId, this.replyFor(prompt));
|
|
20583
21048
|
await this.todoList(turn);
|
|
20584
21049
|
if (prompt.includes("[mcp]")) await this.mcpToolCall(turn);
|
|
20585
|
-
|
|
21050
|
+
const readOnly = this.autonomy === "read_only";
|
|
21051
|
+
if ((!readOnly && (this.fake.approval || prompt.includes("[approval]")) && this.autonomy !== "full" ? await this.approval(turn) : true) && !readOnly) await this.commandToolCall(turn);
|
|
20586
21052
|
if (this.fake.subagent || prompt.includes("[subagent]")) await this.subagent(turn);
|
|
20587
21053
|
if (this.fake.followups ?? true) {
|
|
20588
21054
|
await this.step(turn);
|
|
@@ -20975,7 +21441,8 @@ const capabilities = {
|
|
|
20975
21441
|
reasoningEffort: false,
|
|
20976
21442
|
mcp: false,
|
|
20977
21443
|
skills: false,
|
|
20978
|
-
liveAutonomy: true
|
|
21444
|
+
liveAutonomy: true,
|
|
21445
|
+
readOnly: true
|
|
20979
21446
|
};
|
|
20980
21447
|
/**
|
|
20981
21448
|
* Deterministic scripted provider for demos and tests (§7.2). Every turn streams:
|
|
@@ -26296,6 +26763,303 @@ async function listPorts(probe = []) {
|
|
|
26296
26763
|
url: `http://localhost:${port}`
|
|
26297
26764
|
}));
|
|
26298
26765
|
}
|
|
26766
|
+
// ../runner/src/util/paths.ts
|
|
26767
|
+
/** True when `child` is `dir` itself or somewhere inside it. */
|
|
26768
|
+
function isInside(dir, child) {
|
|
26769
|
+
const rel = relative(resolve(dir), resolve(child));
|
|
26770
|
+
return rel === "" || !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel);
|
|
26771
|
+
}
|
|
26772
|
+
// ../runner/src/workspace/delete-tree.ts
|
|
26773
|
+
/** Error codes Windows returns while another process still holds a file or folder open. */
|
|
26774
|
+
const IN_USE = /* @__PURE__ */ new Set([
|
|
26775
|
+
"EBUSY",
|
|
26776
|
+
"EPERM",
|
|
26777
|
+
"EACCES",
|
|
26778
|
+
"ENOTEMPTY"
|
|
26779
|
+
]);
|
|
26780
|
+
const errorCode = (error) => error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
26781
|
+
/** Run `op`, retrying briefly (about 1.5s for 5 attempts) while the path is in use. */
|
|
26782
|
+
async function retry(op, attempts = 5) {
|
|
26783
|
+
for (let attempt = 1;; attempt++) try {
|
|
26784
|
+
await op();
|
|
26785
|
+
return;
|
|
26786
|
+
} catch (error) {
|
|
26787
|
+
const code = errorCode(error);
|
|
26788
|
+
if (code === "ENOENT") return;
|
|
26789
|
+
if (!code || !IN_USE.has(code) || attempt >= attempts) throw error;
|
|
26790
|
+
await new Promise((r) => setTimeout(r, 100 * attempt));
|
|
26791
|
+
}
|
|
26792
|
+
}
|
|
26793
|
+
/** Delete a file or link (never what a link points at); clears the read-only flag if needed. */
|
|
26794
|
+
function removeFile(path) {
|
|
26795
|
+
return retry(async () => {
|
|
26796
|
+
try {
|
|
26797
|
+
await unlink(path);
|
|
26798
|
+
} catch (error) {
|
|
26799
|
+
if (errorCode(error) !== "EPERM" && errorCode(error) !== "EACCES") throw error;
|
|
26800
|
+
await chmod(path, 438).catch(() => void 0);
|
|
26801
|
+
await unlink(path);
|
|
26802
|
+
}
|
|
26803
|
+
});
|
|
26804
|
+
}
|
|
26805
|
+
async function kind(path, entry) {
|
|
26806
|
+
if (entry.isSymbolicLink()) return "other";
|
|
26807
|
+
if (entry.isDirectory()) return "dir";
|
|
26808
|
+
if (entry.isFile()) return "other";
|
|
26809
|
+
const stat = await lstat(path).catch(() => void 0);
|
|
26810
|
+
return stat?.isDirectory() && !stat.isSymbolicLink() ? "dir" : "other";
|
|
26811
|
+
}
|
|
26812
|
+
/**
|
|
26813
|
+
* Empty `dir`: files and links are unlinked, real folders are emptied and removed. A symlink
|
|
26814
|
+
* or junction is never descended into (Windows reports junctions as links), so nothing
|
|
26815
|
+
* outside the tree is touched. Every branch is finished before the first failure is thrown.
|
|
26816
|
+
*/
|
|
26817
|
+
async function emptyDir(dir) {
|
|
26818
|
+
let entries;
|
|
26819
|
+
try {
|
|
26820
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
26821
|
+
} catch (error) {
|
|
26822
|
+
if (errorCode(error) === "ENOENT") return;
|
|
26823
|
+
throw error;
|
|
26824
|
+
}
|
|
26825
|
+
const failed = (await Promise.allSettled(entries.map(async (entry) => {
|
|
26826
|
+
const path = join(dir, entry.name);
|
|
26827
|
+
if (await kind(path, entry) !== "dir") return removeFile(path);
|
|
26828
|
+
await emptyDir(path);
|
|
26829
|
+
await retry(() => rmdir(path));
|
|
26830
|
+
}))).find((r) => r.status === "rejected");
|
|
26831
|
+
if (failed) throw failed.reason;
|
|
26832
|
+
}
|
|
26833
|
+
/** A short, user-facing error for a failed delete or move of the worktree folder `root`. */
|
|
26834
|
+
function deleteFailed(root, error) {
|
|
26835
|
+
const file = error && typeof error === "object" && "path" in error ? String(error.path) : root;
|
|
26836
|
+
const where = relative(root, file);
|
|
26837
|
+
const code = errorCode(error);
|
|
26838
|
+
if (code && IN_USE.has(code)) return new CommandError("worktree_in_use", `${where ? `A file in the worktree is in use (${where})` : "The worktree is in use"}. Close whatever is using it (a terminal, dev server or editor), then try again.`);
|
|
26839
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
26840
|
+
return new CommandError("remove_failed", `Could not delete ${where || root}: ${reason}`);
|
|
26841
|
+
}
|
|
26842
|
+
/**
|
|
26843
|
+
* Delete a worktree folder without ever following a symlink or junction out of it.
|
|
26844
|
+
*
|
|
26845
|
+
* `git worktree remove` can't be trusted with this on Windows: Git treats junctions as
|
|
26846
|
+
* folders, so it deletes the contents of whatever a live junction points at, and it stops at
|
|
26847
|
+
* a dangling one with "Directory not empty". In a pnpm install the packages are deleted before
|
|
26848
|
+
* the junctions that point at them, so it fails halfway, after removing `.git`. Node's
|
|
26849
|
+
* recursive `rm` is no better here: on Windows it waits for as long as a file stays locked.
|
|
26850
|
+
*
|
|
26851
|
+
* `.git` goes first, so a delete that a locked file interrupts leaves an orphan folder the
|
|
26852
|
+
* next attempt cleans up (never a damaged worktree that looks dirty). Each locked entry is
|
|
26853
|
+
* retried for about 1.5s, then this throws `worktree_in_use`.
|
|
26854
|
+
*/
|
|
26855
|
+
async function deleteTree(root) {
|
|
26856
|
+
try {
|
|
26857
|
+
const stat = await lstat(root).catch(() => void 0);
|
|
26858
|
+
if (!stat) return;
|
|
26859
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return await removeFile(root);
|
|
26860
|
+
await removeFile(join(root, ".git"));
|
|
26861
|
+
await emptyDir(root);
|
|
26862
|
+
await retry(() => rmdir(root));
|
|
26863
|
+
} catch (error) {
|
|
26864
|
+
throw deleteFailed(root, error);
|
|
26865
|
+
}
|
|
26866
|
+
}
|
|
26867
|
+
// ../runner/src/workspace/trash.ts
|
|
26868
|
+
/**
|
|
26869
|
+
* True for a session folder CodeDen created and owns: exactly `<home>/wt/<project>/<session>`.
|
|
26870
|
+
* Only these may be deleted once they are no longer git worktrees.
|
|
26871
|
+
*/
|
|
26872
|
+
function isManagedWorktreePath(home, path) {
|
|
26873
|
+
const rel = relative(resolve(home, "wt"), resolve(path));
|
|
26874
|
+
if (!rel || isAbsolute(rel)) return false;
|
|
26875
|
+
const parts = rel.split(/[\\/]+/);
|
|
26876
|
+
return parts.length === 2 && !parts.includes("..") && !parts.includes(".") && parts[0] !== ".trash";
|
|
26877
|
+
}
|
|
26878
|
+
/** Where removed worktrees wait to be deleted. `.trash` can never be a project slug. */
|
|
26879
|
+
function trashDir(home) {
|
|
26880
|
+
return join(resolve(home), "wt", ".trash");
|
|
26881
|
+
}
|
|
26882
|
+
/**
|
|
26883
|
+
* Move a session folder into the trash. A rename is all or nothing: while anything in the
|
|
26884
|
+
* folder is held open (a process's working directory, an open file), Windows refuses it and
|
|
26885
|
+
* the folder stays exactly as it was (`worktree_in_use`). Returns null when the trash is on
|
|
26886
|
+
* another volume.
|
|
26887
|
+
*/
|
|
26888
|
+
async function moveToTrash(home, path) {
|
|
26889
|
+
const trash = trashDir(home);
|
|
26890
|
+
await mkdir(trash, { recursive: true });
|
|
26891
|
+
const stamp = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
26892
|
+
const dest = join(trash, `${basename(dirname(path))}-${basename(path)}-${stamp}`);
|
|
26893
|
+
try {
|
|
26894
|
+
await retry(() => rename(path, dest), 8);
|
|
26895
|
+
return dest;
|
|
26896
|
+
} catch (error) {
|
|
26897
|
+
if (errorCode(error) === "EXDEV") return null;
|
|
26898
|
+
throw deleteFailed(path, error);
|
|
26899
|
+
}
|
|
26900
|
+
}
|
|
26901
|
+
const runs = /* @__PURE__ */ new Map();
|
|
26902
|
+
/**
|
|
26903
|
+
* Delete everything in the trash, one folder at a time, never following a link (the links in
|
|
26904
|
+
* a trashed pnpm install still point at the old path, where a new worktree may live by now).
|
|
26905
|
+
* Runs in the background after each removal and when the runner starts; a folder something
|
|
26906
|
+
* still holds open stays for the next run. Calls during a run share it and add one more pass.
|
|
26907
|
+
*/
|
|
26908
|
+
function emptyTrash(home, log) {
|
|
26909
|
+
const trash = trashDir(home);
|
|
26910
|
+
const current = runs.get(trash);
|
|
26911
|
+
if (current) {
|
|
26912
|
+
current.again = true;
|
|
26913
|
+
return current.done;
|
|
26914
|
+
}
|
|
26915
|
+
const run = {
|
|
26916
|
+
again: true,
|
|
26917
|
+
done: Promise.resolve()
|
|
26918
|
+
};
|
|
26919
|
+
run.done = (async () => {
|
|
26920
|
+
try {
|
|
26921
|
+
while (run.again) {
|
|
26922
|
+
run.again = false;
|
|
26923
|
+
const entries = await readdir(trash).catch(() => []);
|
|
26924
|
+
for (const name of entries) await deleteTree(join(trash, name)).catch((error) => log?.(`worktree trash: ${error instanceof Error ? error.message : String(error)}`));
|
|
26925
|
+
}
|
|
26926
|
+
} finally {
|
|
26927
|
+
runs.delete(trash);
|
|
26928
|
+
}
|
|
26929
|
+
})();
|
|
26930
|
+
runs.set(trash, run);
|
|
26931
|
+
return run.done;
|
|
26932
|
+
}
|
|
26933
|
+
/**
|
|
26934
|
+
* Delete a worktree folder that has been cleared for removal. A session folder under
|
|
26935
|
+
* `<home>/wt` goes to the trash, so the removal is instant and the path is free right away,
|
|
26936
|
+
* and is deleted in the background. Anything else is deleted in place.
|
|
26937
|
+
*/
|
|
26938
|
+
async function discardFolder(home, path, log) {
|
|
26939
|
+
if (isManagedWorktreePath(home, path) ? await moveToTrash(home, path) : null) emptyTrash(home, log);
|
|
26940
|
+
else await deleteTree(path);
|
|
26941
|
+
}
|
|
26942
|
+
// ../runner/src/workspace/removal.ts
|
|
26943
|
+
async function canonical(path) {
|
|
26944
|
+
const real = await realpath(path).catch(() => resolve(path));
|
|
26945
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
26946
|
+
}
|
|
26947
|
+
async function listWorktrees(repo) {
|
|
26948
|
+
const out = await gitText(repo, [
|
|
26949
|
+
"worktree",
|
|
26950
|
+
"list",
|
|
26951
|
+
"--porcelain"
|
|
26952
|
+
]).catch(() => "");
|
|
26953
|
+
const list = [];
|
|
26954
|
+
for (const block of out.split(/\r?\n\r?\n/)) {
|
|
26955
|
+
const lines = block.split(/\r?\n/);
|
|
26956
|
+
if (!lines[0]?.startsWith("worktree ")) continue;
|
|
26957
|
+
const locked = lines.some((l) => l === "locked" || l.startsWith("locked "));
|
|
26958
|
+
list.push({
|
|
26959
|
+
path: lines[0].slice(9),
|
|
26960
|
+
locked
|
|
26961
|
+
});
|
|
26962
|
+
}
|
|
26963
|
+
return list;
|
|
26964
|
+
}
|
|
26965
|
+
/** Classify `path` against the worktrees of `repo` (see `WorktreeState`). */
|
|
26966
|
+
async function worktreeState(repo, path) {
|
|
26967
|
+
const target = await canonical(path);
|
|
26968
|
+
let registered = false;
|
|
26969
|
+
let locked = false;
|
|
26970
|
+
let containsOther = false;
|
|
26971
|
+
for (const wt of await listWorktrees(repo)) {
|
|
26972
|
+
const other = await canonical(wt.path);
|
|
26973
|
+
if (other === target) {
|
|
26974
|
+
registered = true;
|
|
26975
|
+
locked = wt.locked;
|
|
26976
|
+
} else if (isInside(target, other)) containsOther = true;
|
|
26977
|
+
}
|
|
26978
|
+
if (isInside(target, await canonical(repo))) containsOther = true;
|
|
26979
|
+
const info = {
|
|
26980
|
+
registered,
|
|
26981
|
+
locked,
|
|
26982
|
+
containsOther
|
|
26983
|
+
};
|
|
26984
|
+
const stat = await lstat(path).catch(() => void 0);
|
|
26985
|
+
if (!stat) return {
|
|
26986
|
+
state: "missing",
|
|
26987
|
+
...info
|
|
26988
|
+
};
|
|
26989
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return {
|
|
26990
|
+
state: "orphan",
|
|
26991
|
+
...info
|
|
26992
|
+
};
|
|
26993
|
+
const dotGit = await lstat(resolve(path, ".git")).catch(() => void 0);
|
|
26994
|
+
if (!dotGit) return {
|
|
26995
|
+
state: "orphan",
|
|
26996
|
+
...info
|
|
26997
|
+
};
|
|
26998
|
+
return {
|
|
26999
|
+
state: dotGit.isFile() && registered ? "registered" : "foreign",
|
|
27000
|
+
...info
|
|
27001
|
+
};
|
|
27002
|
+
}
|
|
27003
|
+
/** Whether a registered worktree has uncommitted changes (ignored files don't count). */
|
|
27004
|
+
async function isDirty(path) {
|
|
27005
|
+
return (await git(path, ["status", "--porcelain"])).stdout.toString("utf8").trim() !== "";
|
|
27006
|
+
}
|
|
27007
|
+
/**
|
|
27008
|
+
* Decide whether `workspace.remove` may delete the path, before anything is touched (terminals
|
|
27009
|
+
* and processes in it are stopped only after this passes). Refuses (`not_a_worktree`) anything
|
|
27010
|
+
* that isn't a linked worktree of the repository, unless it is an orphan inside CodeDen's
|
|
27011
|
+
* worktree folder, and anything containing the repository or another worktree. Unless forced,
|
|
27012
|
+
* keeps a locked worktree (`worktree_locked`) or one with uncommitted changes
|
|
27013
|
+
* (`worktree_dirty`); ignored files (build output, `node_modules`) don't count.
|
|
27014
|
+
*/
|
|
27015
|
+
async function assertRemovable(cmd, home) {
|
|
27016
|
+
const path = resolve(cmd.path);
|
|
27017
|
+
const info = await worktreeState(cmd.repoPath, path);
|
|
27018
|
+
const leftAlone = (why) => new CommandError("not_a_worktree", `${path} ${why}, so it was left alone`);
|
|
27019
|
+
if (info.state === "missing") return info;
|
|
27020
|
+
if (info.containsOther) throw leftAlone("contains the repository or another worktree");
|
|
27021
|
+
if (info.state === "foreign") throw leftAlone("is not a worktree of this repository");
|
|
27022
|
+
if (info.state === "orphan" && !isManagedWorktreePath(home, path)) throw leftAlone("is no longer a git worktree and is outside CodeDen's worktree folder");
|
|
27023
|
+
if (info.state !== "registered" || cmd.force) return info;
|
|
27024
|
+
if (info.locked) throw new CommandError("worktree_locked", `${path} is locked; unlock it or remove it with force`);
|
|
27025
|
+
if (await isDirty(path)) throw new CommandError("worktree_dirty", `${path} has uncommitted changes; commit them or remove it with force`);
|
|
27026
|
+
return info;
|
|
27027
|
+
}
|
|
27028
|
+
/** Drop git's records of worktrees whose folders are gone. */
|
|
27029
|
+
async function pruneWorktrees(repo) {
|
|
27030
|
+
await git(repo, ["worktree", "prune"]).catch(() => void 0);
|
|
27031
|
+
}
|
|
27032
|
+
/**
|
|
27033
|
+
* `workspace.remove`: delete a session worktree and git's record of it. Idempotent: a missing
|
|
27034
|
+
* path, or a half-deleted orphan inside `<home>/wt`, ends removed too. The folder goes to the
|
|
27035
|
+
* trash or is deleted in place, never through a link (`discardFolder`), then
|
|
27036
|
+
* `git worktree prune` drops the record. The branch stays unless `deleteBranch` names it.
|
|
27037
|
+
*/
|
|
27038
|
+
async function removeWorkspace(cmd, home, log) {
|
|
27039
|
+
const path = resolve(cmd.path);
|
|
27040
|
+
const info = await assertRemovable(cmd, home);
|
|
27041
|
+
const registered = info.state === "registered";
|
|
27042
|
+
const discardedChanges = registered && cmd.force === true && await isDirty(path);
|
|
27043
|
+
try {
|
|
27044
|
+
if (registered && info.locked) await git(cmd.repoPath, [
|
|
27045
|
+
"worktree",
|
|
27046
|
+
"unlock",
|
|
27047
|
+
path
|
|
27048
|
+
]).catch(() => void 0);
|
|
27049
|
+
if (info.state !== "missing") await discardFolder(home, path, log);
|
|
27050
|
+
} finally {
|
|
27051
|
+
await pruneWorktrees(cmd.repoPath);
|
|
27052
|
+
}
|
|
27053
|
+
if (cmd.deleteBranch) await git(cmd.repoPath, [
|
|
27054
|
+
"branch",
|
|
27055
|
+
"-D",
|
|
27056
|
+
cmd.deleteBranch
|
|
27057
|
+
]).catch(() => void 0);
|
|
27058
|
+
return {
|
|
27059
|
+
removed: true,
|
|
27060
|
+
...discardedChanges ? { discardedChanges } : {}
|
|
27061
|
+
};
|
|
27062
|
+
}
|
|
26299
27063
|
// ../runner/src/workspace/project-config.ts
|
|
26300
27064
|
const PROJECT_CONFIG_PATH = ".codeden/project.json";
|
|
26301
27065
|
/**
|
|
@@ -26438,20 +27202,41 @@ async function resolveBase(repo, cmd) {
|
|
|
26438
27202
|
])) return `origin/${def}`;
|
|
26439
27203
|
return def ?? "HEAD";
|
|
26440
27204
|
}
|
|
26441
|
-
|
|
27205
|
+
function isEmptyDir(path) {
|
|
27206
|
+
try {
|
|
27207
|
+
return readdirSync(path).length === 0;
|
|
27208
|
+
} catch {
|
|
27209
|
+
return false;
|
|
27210
|
+
}
|
|
27211
|
+
}
|
|
27212
|
+
/**
|
|
27213
|
+
* Make `path` ready for `git worktree add`: reuse a worktree of this repository, delete the
|
|
27214
|
+
* orphan folder an interrupted removal left inside `<home>/wt` (stopping processes in it
|
|
27215
|
+
* first), and drop git's stale record of it. Returns the worktree to reuse, if any.
|
|
27216
|
+
*/
|
|
27217
|
+
async function clearTarget(repo, path, ctx) {
|
|
27218
|
+
const info = await worktreeState(repo, path);
|
|
27219
|
+
if (info.state === "registered") return {
|
|
27220
|
+
cwd: path,
|
|
27221
|
+
branch: await currentBranch(path),
|
|
27222
|
+
created: false
|
|
27223
|
+
};
|
|
27224
|
+
if (info.state === "orphan" && isManagedWorktreePath(ctx.home, path) && !info.containsOther) {
|
|
27225
|
+
await ctx.supervisor.killUnder(path);
|
|
27226
|
+
await discardFolder(ctx.home, path);
|
|
27227
|
+
} else if (info.state !== "missing" && !isEmptyDir(path)) throw new CommandError("path_exists", `${path} exists and is not a worktree`);
|
|
27228
|
+
if (info.registered) await pruneWorktrees(repo);
|
|
27229
|
+
return null;
|
|
27230
|
+
}
|
|
27231
|
+
async function createWorktree(cmd, ctx) {
|
|
26442
27232
|
const repo = await topLevel(cmd.repoPath).catch(() => {
|
|
26443
27233
|
throw new CommandError("not_a_repo", `${cmd.repoPath} is not a git repository`);
|
|
26444
27234
|
});
|
|
26445
27235
|
const short = sessionShort(cmd.sessionId);
|
|
26446
|
-
const
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
branch: await currentBranch(path),
|
|
26451
|
-
created: false
|
|
26452
|
-
};
|
|
26453
|
-
if (readdirSync(path).length > 0) throw new CommandError("path_exists", `${path} exists and is not a worktree`);
|
|
26454
|
-
}
|
|
27236
|
+
const slug = cmd.projectSlug ?? slugFromPath(repo);
|
|
27237
|
+
const path = resolve(cmd.path ?? join(ctx.home, "wt", slug, short));
|
|
27238
|
+
const reuse = await clearTarget(repo, path, ctx);
|
|
27239
|
+
if (reuse) return reuse;
|
|
26455
27240
|
await mkdir(dirname(path), { recursive: true });
|
|
26456
27241
|
const base = await resolveBase(repo, cmd);
|
|
26457
27242
|
if (cmd.branch && await branchExists(repo, cmd.branch)) {
|
|
@@ -26511,7 +27296,7 @@ async function prepareWorkspace(cmd, ctx) {
|
|
|
26511
27296
|
created: false
|
|
26512
27297
|
};
|
|
26513
27298
|
break;
|
|
26514
|
-
case "worktree": target = await createWorktree(cmd, ctx
|
|
27299
|
+
case "worktree": target = await createWorktree(cmd, ctx);
|
|
26515
27300
|
}
|
|
26516
27301
|
const { config, error } = await readProjectConfig(target.cwd);
|
|
26517
27302
|
if (error) ctx.emit({
|
|
@@ -26536,53 +27321,6 @@ async function prepareWorkspace(cmd, ctx) {
|
|
|
26536
27321
|
setup
|
|
26537
27322
|
};
|
|
26538
27323
|
}
|
|
26539
|
-
/**
|
|
26540
|
-
* Refuse to remove a worktree with uncommitted changes (`worktree_dirty`) unless forced. Runs
|
|
26541
|
-
* before terminals and processes in the worktree are stopped, so a dirty worktree is left
|
|
26542
|
-
* exactly as it was. Ignored files (build output, `node_modules`) don't count.
|
|
26543
|
-
*/
|
|
26544
|
-
async function assertRemovable(cmd) {
|
|
26545
|
-
if (cmd.force || !existsSync(cmd.path) || !await isWorkTree(cmd.path)) return;
|
|
26546
|
-
if ((await git(cmd.path, ["status", "--porcelain"])).stdout.toString("utf8").trim()) throw new CommandError("worktree_dirty", `${cmd.path} has uncommitted changes; commit them or remove it with force`);
|
|
26547
|
-
}
|
|
26548
|
-
/** `workspace.remove`: remove a worktree (retrying briefly for Windows file locks). */
|
|
26549
|
-
async function removeWorkspace(cmd) {
|
|
26550
|
-
const path = resolve(cmd.path);
|
|
26551
|
-
let removed = false;
|
|
26552
|
-
let lastError;
|
|
26553
|
-
for (let attempt = 0; attempt < 5 && !removed; attempt++) try {
|
|
26554
|
-
const args = [
|
|
26555
|
-
"worktree",
|
|
26556
|
-
"remove",
|
|
26557
|
-
...cmd.force ? ["--force"] : [],
|
|
26558
|
-
path
|
|
26559
|
-
];
|
|
26560
|
-
await git(cmd.repoPath, args);
|
|
26561
|
-
removed = true;
|
|
26562
|
-
} catch (error) {
|
|
26563
|
-
lastError = error;
|
|
26564
|
-
if (!existsSync(path)) break;
|
|
26565
|
-
if (!cmd.force && /modified or untracked|contains/.test(String(error))) break;
|
|
26566
|
-
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
26567
|
-
}
|
|
26568
|
-
if (!removed && existsSync(path) && cmd.force) {
|
|
26569
|
-
await rm(path, {
|
|
26570
|
-
recursive: true,
|
|
26571
|
-
force: true,
|
|
26572
|
-
maxRetries: 5,
|
|
26573
|
-
retryDelay: 200
|
|
26574
|
-
});
|
|
26575
|
-
removed = true;
|
|
26576
|
-
}
|
|
26577
|
-
await git(cmd.repoPath, ["worktree", "prune"]).catch(() => void 0);
|
|
26578
|
-
if (!removed && existsSync(path)) throw lastError instanceof Error ? lastError : new CommandError("remove_failed", `Could not remove ${path}`);
|
|
26579
|
-
if (cmd.deleteBranch) await git(cmd.repoPath, [
|
|
26580
|
-
"branch",
|
|
26581
|
-
"-D",
|
|
26582
|
-
cmd.deleteBranch
|
|
26583
|
-
]).catch(() => void 0);
|
|
26584
|
-
return { removed: removed || !existsSync(path) };
|
|
26585
|
-
}
|
|
26586
27324
|
// ../runner/src/commands.ts
|
|
26587
27325
|
/** Execute one server command (§4.3). Throws `CommandError` for expected failures. */
|
|
26588
27326
|
async function executeCommand(core, command) {
|
|
@@ -26632,10 +27370,10 @@ async function executeCommand(core, command) {
|
|
|
26632
27370
|
emit: core.emit
|
|
26633
27371
|
});
|
|
26634
27372
|
case "workspace.remove":
|
|
26635
|
-
await assertRemovable(command);
|
|
27373
|
+
await assertRemovable(command, core.home);
|
|
26636
27374
|
await terminals.closeUnder(command.path);
|
|
26637
27375
|
await core.supervisor.killUnder(command.path);
|
|
26638
|
-
return removeWorkspace(command);
|
|
27376
|
+
return removeWorkspace(command, core.home, core.log);
|
|
26639
27377
|
case "session.start":
|
|
26640
27378
|
if (core.isDraining()) throw new CommandError("draining", "Runner is draining");
|
|
26641
27379
|
return host.start(command);
|
|
@@ -26913,12 +27651,6 @@ async function handleMcpRequest(request, upstream) {
|
|
|
26913
27651
|
return jsonRpcError(502, `CodeDen server unreachable: ${error.message}`);
|
|
26914
27652
|
}
|
|
26915
27653
|
}
|
|
26916
|
-
// ../runner/src/util/paths.ts
|
|
26917
|
-
/** True when `child` is `dir` itself or somewhere inside it. */
|
|
26918
|
-
function isInside(dir, child) {
|
|
26919
|
-
const rel = relative(resolve(dir), resolve(child));
|
|
26920
|
-
return rel === "" || !rel.startsWith("..") && !/^[a-zA-Z]:/.test(rel);
|
|
26921
|
-
}
|
|
26922
27654
|
// ../runner/src/process/supervisor.ts
|
|
26923
27655
|
/**
|
|
26924
27656
|
* Tracks every child process the runner starts so they can be killed as whole process
|
|
@@ -28721,6 +29453,7 @@ async function startRunner(opts) {
|
|
|
28721
29453
|
browser,
|
|
28722
29454
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28723
29455
|
emit,
|
|
29456
|
+
log,
|
|
28724
29457
|
isDraining: () => draining,
|
|
28725
29458
|
drain: ({ reason, timeoutMs, exitWhenDrained }) => {
|
|
28726
29459
|
if (!draining) {
|
|
@@ -28794,6 +29527,7 @@ async function startRunner(opts) {
|
|
|
28794
29527
|
renameSync(tmp, lockFile);
|
|
28795
29528
|
log(`runner ${journal.runnerId} listening on 127.0.0.1:${port}`);
|
|
28796
29529
|
registry.refreshModels();
|
|
29530
|
+
emptyTrash(opts.home, log);
|
|
28797
29531
|
const removeLock = () => removeOwnLock(lockFile);
|
|
28798
29532
|
process.once("exit", removeLock);
|
|
28799
29533
|
async function close() {
|
|
@@ -28824,6 +29558,6 @@ async function startRunner(opts) {
|
|
|
28824
29558
|
close
|
|
28825
29559
|
};
|
|
28826
29560
|
}
|
|
28827
|
-
export {
|
|
29561
|
+
export { BrowserNavigateRequest as $, AgentConfigLocator as $n, NOTICE_BATCH_CODE as $t, SessionFileQuery as A, GitCommitRequest as An, RespondRequest as At, NetworkStatus as B, ConflictDecision as Bn, HealthResponse as Bt, isElectronRuntime as C, AutomationRun as Cn, string as Cr, CreateProjectRequest as Ct, CODEDEN_VERSION as D, SessionStatus as Dn, toJSONSchema as Dr, ListSessionsQuery as Dt, wrapper_default as E, Session as En, ZodError as Er, ListInboxQuery as Et, ListConflictsQuery as F, TerminalClientMessage as Fn, StartSessionRequest as Ft, SetNetworkPasswordResponse as G, DEFAULT_VISION_PATH as Gn, SttQuery as Gt, RemoteAccessFile as H, WorkKey as Hn, Settings as Ht, ListNoticesQuery as I, TerminalQuery as In, SteerRequest as It, ImageTestRequest as J, InboxItemKind as Jn, UpdateInstallRequest as Jt, SignInRequest as K, FactoryLimits as Kn, TtsRequest as Kt, ResolveConflictRequest as L, RunnerCommandResults as Ln, ThreadItemsQuery as Lt, RunnerToServerMessage as M, RestoreFilesRequest as Mn, SendMessageRequest as Mt, ServerLock as N, SessionResult as Nn, SessionStartMeta as Nt, UsageQuery as O, DiffScope as On, safeParse$1 as Or, ReorderProjectsRequest as Ot, ArchiveSessionRequest as P, SpeechSummaryRequest as Pn, SetSecretRequest as Pt, BrowserHistoryRequest as Q, AgentConfigListQuery as Qn, ConflictRecord as Qt, RoleMessageRequest as R, SessionOptions as Rn, UiClientMessage as Rt, acquireInstallLock as S, AutomationGuards as Sn, record as Sr, UpdateAutomationRequest as St, import_websocket_server as T, TriagePolicy as Tn, unknown as Tr, FileWriteRequest as Tt, SetNetworkAccessRequest as U, Project as Un, LOCAL_STT_SAMPLE_RATE as Ut, PASSWORD_RULE_TEXT as V, StartedBy as Vn, agentEventToNewEvent as Vt, SetNetworkPasswordRequest as W, sortProjects as Wn, LocalSttModelParams as Wt, BrowserClientMessage as X, AgentConfigDeleteQuery as Xn, compareVersions$1 as Xt, summarizeActivity as Y, messageFrom as Yn, UpdateStatus as Yt, BrowserDevServerRequest as Z, AgentConfigDiffRequest as Zn, isNewerVersion as Zt, importClaudeSdkFrom as _, SetAutonomyModeRequest as _n, looseObject as _r, StarterSetRequest as _t, Hono as a, GithubPr as an, ProviderId as ar, AutomationImportSource as at, runNpm as b, Automation as bn, optional as br, UpcomingRunsQuery as bt, createAdaptorServer as c, githubPrId as cn, _enum as cr, ImportAutomationsRequest as ct, createImageEngineRegistry as d, FactoryCandidate as dn, boolean as dr, AutomationProjectRequest as dt, CreateGithubIssueRequest as en, AgentConfigWriteRequest as er, BrowserNewTabRequest as et, IMAGE_ASPECT_RATIOS as f, FactoryTriageRequest as fn, custom as fr, CreateAutomationRequest as ft, CLAUDE_SDK_VERSION as g, ReviewVerdict as gn, literal as gr, SetAutomationEnabledRequest as gt, claudeSdkSource as h, PutVisionRequest as hn, intersection as hr, RunAutomationRequest as ht, killTree as i, GithubListQuery as in, AutonomyLevel as ir, AUTOMATION_IMPORT_LIMITS as it, RunnerLock as j, GitPushRequest as jn, SecretKey as jt, UsageSettings as k, EditQueuedMessageRequest as kn, prettifyError as kr, ResolveInboxRequest as kt, upgradeWebSocket as l, parseGithubPrId as ln, _null as lr, AutomationFileFrontmatter as lt, ImageGenerationError as m, KillSwitchRequest as mn, int as mr, ListAutomationsQuery as mt, readLock as n, GithubIssue as nn, McpServerTarget as nr, BrowserViewportRequest as nt, tryDecodeURIComponent as o, MergePrRequest as on, ReasoningEffort as or, DuplicateAutomationRequest as ot, resolveImageOutput as p, FactoryTuning as pn, discriminatedUnion as pr, ListAutomationRunsQuery as pt, passwordProblem as q, InboxItem as qn, AppPointer as qt, isAlive as r, GithubLabelRequest as rn, McpServerUpsertRequest as rr, BrowserWsQuery as rt, bufferToFormData as s, TriageIssueRequest as sn, datetime as sr, ExportAutomationsQuery as st, startRunner as t, GithubCommentRequest as tn, McpServerDiffRequest as tr, BrowserOpenRequest as tt, ImageEngineRegistry as u, DiscoveryArea as un, array as ur, AutomationGuardsPatch as ut, installClaudeSdk as v, isFactoryMode as vn, number$1 as vr, TriageDecisionRequest as vt, ownNodeEnv as w, AutomationTrigger as wn, union as wr, FileReadQuery as wt, resolveNpm as x, AutomationAction as xn, preprocess as xr, UpdateAutomationProjectSettingsRequest as xt, isClaudeSdkInstalled as y, Artifact as yn, object as yr, TriageReport as yt, UpdateSessionRequest as z, ProjectConfigFile as zn, UpdateProjectRequest as zt };
|
|
28828
29562
|
|
|
28829
|
-
//# sourceMappingURL=runner-
|
|
29563
|
+
//# sourceMappingURL=runner-QPAnitMx.js.map
|