aegis-desktop 0.8.34 → 1.0.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 CHANGED
@@ -101,6 +101,17 @@ the models that key unlocks appear as their own entries, one per provider.
101
101
  startup, and a provider with no key saved in Settings is resolved from it.
102
102
  3. Select the **Bring your own key** class and pick a model.
103
103
 
104
+ **A model the catalogue does not name yet.** Vendors ship models faster than any
105
+ client release, and the relay forwards whatever model string it is handed — so
106
+ Settings ends with an **Add model** row (provider + model id + *Add model*). It
107
+ writes the same `~/.aegiscode/models.json` the CLI's `/model-add` writes, so a
108
+ model added here appears in the terminal's `/models` immediately and one added
109
+ there appears in this picker on its next refresh; the row prints the file path,
110
+ which is also the bulk-edit path. The provider id is offered from the live
111
+ catalogue but you may type one, and a provider the relay takes no key for is
112
+ refused rather than stored (a declaration nothing can attach a key to would show
113
+ up in no picker at all). Each declared model gets a **Remove** row.
114
+
104
115
  **What it costs.** AEGIS pays your provider nothing on this lane, so there is no
105
116
  provider cost to take a margin on — instead your AEGIS account is charged a flat
106
117
  **handling fee** per 1k tokens, for the routing, prompt assembly, caching, tool
package/lib/deep-link.js CHANGED
@@ -14,6 +14,36 @@
14
14
  const PROTOCOL = 'aegis';
15
15
  const SCHEME_PREFIX = `${PROTOCOL}://`;
16
16
 
17
+ /**
18
+ * Ceilings on the two values a *remote* caller controls.
19
+ *
20
+ * Any web page or any other app on the machine can hand the OS an `aegis://`
21
+ * URL, so both fields are attacker-chosen input on a channel nobody authenticates
22
+ * (there is no "is this link from the user?" check, and there cannot be one for
23
+ * a custom scheme). Bounding them keeps a hostile link from parking megabytes of
24
+ * text in the composer, from filling the window with a wall of pre-filled prompt,
25
+ * or from using the deep link as a bulk data channel into a session that can run
26
+ * tools. The renderer pre-fills rather than auto-sends (renderer/app.js
27
+ * handleDeepLink), so the cost is bounded, but a pre-filled prompt is still the
28
+ * most direct route from "a page I visited" to "text the user is invited to send
29
+ * to a model with tools on".
30
+ *
31
+ * These are generous on purpose: a real hand-written link is a sentence or two.
32
+ */
33
+ const MAX_PROMPT_LENGTH = 2000;
34
+ /** Session ids are uuids (36 chars); the slack is for a future format, not a payload. */
35
+ const MAX_SESSION_ID_LENGTH = 128;
36
+ /**
37
+ * …and the charset is pinned too, because this id does not stay a string: the
38
+ * renderer's `open` handler hands it to openSession(), which loads the session
39
+ * by name. A hostile link could otherwise offer `../../…` or an absolute path
40
+ * and have the *renderer* ask main to open whatever that resolves to. Ids this
41
+ * app mints are `[0-9a-f-]`; anything outside a conservative id alphabet is not
42
+ * one of ours, so it is rejected rather than escaped or repaired.
43
+ */
44
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
45
+
46
+
17
47
  /** True for any string that looks like our protocol, cheap enough to filter
18
48
  * argv with before the full URL parse (which throws on most argv entries —
19
49
  * flags, file paths — so callers should not run it against every arg). */
@@ -44,11 +74,15 @@ function parseDeepLinkUrl(url) {
44
74
  if (action === 'open') {
45
75
  const sessionId = parsed.searchParams.get('session');
46
76
  if (!sessionId) return null;
77
+ if (sessionId.length > MAX_SESSION_ID_LENGTH) return null;
78
+ if (!SESSION_ID_PATTERN.test(sessionId)) return null;
47
79
  return { action: 'open', sessionId };
48
80
  }
49
81
 
50
82
  if (action === 'new') {
51
- return { action: 'new', prompt: parsed.searchParams.get('prompt') || '' };
83
+ const prompt = parsed.searchParams.get('prompt') || '';
84
+ if (prompt.length > MAX_PROMPT_LENGTH) return null;
85
+ return { action: 'new', prompt };
52
86
  }
53
87
 
54
88
  return null;
@@ -73,6 +73,83 @@ const MAX_ROUND_STOPS = 3;
73
73
  */
74
74
  const WRITE_TOOLS = new Set(['writeFile', 'editFile']);
75
75
 
76
+ // ── Auto mode and the round horizon ─────────────────────────────────────────
77
+ //
78
+ // The two halves of "auto mode" the upstream engine (aegiscodex-dev) defines
79
+ // are independent, and a host that wires only one of them is *not* running auto
80
+ // mode — it is running "no approval cards", which is half the feature and all
81
+ // the friction:
82
+ //
83
+ // 1. no approval keystroke per mutating tool call;
84
+ // 2. a turn that hits its round horizon is EXTENDED silently a few times
85
+ // before a human is asked.
86
+ //
87
+ // (1) is the posture (the desktop's Tool-approvals switch, the CLI's
88
+ // /automode); (2) needs the arithmetic below, and the plugin's Electron host
89
+ // had none of it — with auto mode ON, every horizon hit still raised the
90
+ // blocking round card, which is precisely the question auto mode exists to stop
91
+ // asking ("12 rounds — keep going?" answered the same way every time).
92
+ //
93
+ // Ported verbatim from the engine's src/autonomous.js (DEFAULT_AUTO_CONTINUES,
94
+ // autoContinueLimit, autoRoundsDefault, autoContinuePlan) rather than vendored,
95
+ // because this repo hosts no build of that ESM module — but with the SAME
96
+ // numbers and the SAME env knobs, so a turn behaves identically in either
97
+ // client. cli/src/chatflow.js carries the third copy, which is what the CLI
98
+ // presenter uses to answer the ask.
99
+
100
+ /** Automatic continuations an attended turn may take while auto mode is on. */
101
+ const DEFAULT_AUTO_CONTINUES = 3;
102
+
103
+ /**
104
+ * The auto-continue allowance, honouring AEGIS_AUTO_CONTINUE_MAX.
105
+ *
106
+ * Bounded and pure, and a garbage value falls back to the default rather than
107
+ * to 0: an unparseable env var must not silently switch auto mode off, because
108
+ * the visible symptom of that ("why is it asking me again?") points nowhere
109
+ * near the environment.
110
+ */
111
+ function autoContinueLimit(env = process.env) {
112
+ const n = Number.parseInt(env && env.AEGIS_AUTO_CONTINUE_MAX, 10);
113
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_AUTO_CONTINUES;
114
+ }
115
+
116
+ /**
117
+ * Values of AEGIS_AUTO_ROUNDS that mean "start in manual mode". Anything else —
118
+ * unset, empty, a typo like `flase` — leaves auto mode's default in place, so a
119
+ * misspelt opt-out cannot silently disable the mode the operator asked for.
120
+ * Same set, same reasoning, as the engine's AUTO_ROUNDS_OFF.
121
+ */
122
+ const AUTO_ROUNDS_OFF = new Set(['0', 'false', 'off', 'no']);
123
+
124
+ /** False only when AEGIS_AUTO_ROUNDS explicitly says so. */
125
+ function autoRoundsDefault(env = process.env) {
126
+ const raw = env && env.AEGIS_AUTO_ROUNDS;
127
+ if (raw === undefined || raw === null) return true;
128
+ const val = String(raw).trim().toLowerCase();
129
+ if (!val) return true;
130
+ return !AUTO_ROUNDS_OFF.has(val);
131
+ }
132
+
133
+ /**
134
+ * Decide what one round-horizon ask becomes while auto mode is on.
135
+ *
136
+ * Pure, because this is budget arithmetic on a keystroke-less path: the caller
137
+ * only renders the answer and must not be trusted to keep the count. `used` is
138
+ * the running tally the caller holds and echoes back, so an ask that IS shown
139
+ * to the human resets it — the human was asked, so the next block of automatic
140
+ * continuations starts clean.
141
+ *
142
+ * { auto: true, used: n } → answer the ask "yes" without a dialog
143
+ * { auto: false, used: n } → show the real ask (allowance spent, or off)
144
+ */
145
+ function autoContinuePlan({ on = false, used = 0, limit = DEFAULT_AUTO_CONTINUES } = {}) {
146
+ const cap = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : DEFAULT_AUTO_CONTINUES;
147
+ const spent = Number.isFinite(used) && used > 0 ? Math.floor(used) : 0;
148
+ if (!on) return { auto: false, used: spent, cap };
149
+ if (spent >= cap) return { auto: false, used: 0, cap };
150
+ return { auto: true, used: spent + 1, cap };
151
+ }
152
+
76
153
  /** The path a file-writing tool call names, in either spelling the engine uses. */
77
154
  function writtenPath(tool) {
78
155
  const args = (tool && tool.args) || {};
@@ -906,6 +983,10 @@ module.exports = {
906
983
  isAutonomousRequest,
907
984
  maxRounds,
908
985
  withRoundHorizon,
986
+ DEFAULT_AUTO_CONTINUES,
987
+ autoContinueLimit,
988
+ autoRoundsDefault,
989
+ autoContinuePlan,
909
990
  resolveModel,
910
991
  ignoredEnvModel,
911
992
  resolveEffort,