shadok-ai 0.2.13 → 0.2.14
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.
|
@@ -33,6 +33,24 @@ Result: quiet days cost nothing; the LLM is spent only to word a real alert.
|
|
|
33
33
|
For pure reporting where you WANT output every run (e.g. a daily figures digest),
|
|
34
34
|
omit the check — the prompt runs every time.
|
|
35
35
|
|
|
36
|
+
### What a check can see — secrets and cwd
|
|
37
|
+
|
|
38
|
+
A check does **not** run inside this agent's process. The server runs it with
|
|
39
|
+
`sh -c`, in the CHANNEL's directory, with the secrets of the channel's
|
|
40
|
+
**profile** injected as environment variables.
|
|
41
|
+
|
|
42
|
+
So reference a secret by name (`$MY_API_KEY`) and trust it: never hardcode a
|
|
43
|
+
value into a check script, and never try to source a shell profile to get one.
|
|
44
|
+
|
|
45
|
+
The sharp edge is the other direction. A channel with **no profile**, or a
|
|
46
|
+
profile that doesn't list that name, gets **nothing** — the variable is simply
|
|
47
|
+
absent, and you find out at 6am rather than now. A name that isn't in the vault
|
|
48
|
+
is skipped just as silently. Your own shell having the key proves nothing about
|
|
49
|
+
the guard's.
|
|
50
|
+
|
|
51
|
+
`schedule.py env` prints exactly what a guard gets here. Run it before writing a
|
|
52
|
+
check that needs a secret.
|
|
53
|
+
|
|
36
54
|
## Commands
|
|
37
55
|
|
|
38
56
|
```
|
|
@@ -40,6 +58,7 @@ python3 scripts/schedule.py add --schedule <spec> --prompt "<text>" [--check "<s
|
|
|
40
58
|
python3 scripts/schedule.py list
|
|
41
59
|
python3 scripts/schedule.py del <id>
|
|
42
60
|
python3 scripts/schedule.py tz [<zone>|-]
|
|
61
|
+
python3 scripts/schedule.py env
|
|
43
62
|
```
|
|
44
63
|
|
|
45
64
|
`<spec>`: `every:30m` · `every:2h` · `daily:09:00`.
|
|
@@ -59,6 +59,7 @@ def main():
|
|
|
59
59
|
a.add_argument("--tz", default=None, help="IANA timezone for a daily schedule (e.g. Europe/Paris); default = server setting")
|
|
60
60
|
t = sub.add_parser("tz", help="show or set the default timezone of daily schedules")
|
|
61
61
|
t.add_argument("zone", nargs="?", help="IANA name (e.g. Europe/Paris); omit to show, '-' to clear")
|
|
62
|
+
sub.add_parser("env", help="show the cwd and the secret names a --check guard will get")
|
|
62
63
|
sub.add_parser("list")
|
|
63
64
|
d = sub.add_parser("del")
|
|
64
65
|
d.add_argument("id")
|
|
@@ -89,6 +90,41 @@ def main():
|
|
|
89
90
|
r = api("GET", "/timezone")
|
|
90
91
|
cur = r.get("timezone")
|
|
91
92
|
print(f"daily schedules run in: {cur or r.get('system')}" + ("" if cur else " (machine default — set one to pin it)"))
|
|
93
|
+
elif args.cmd == "env":
|
|
94
|
+
# A guard runs server-side, not in this agent's process: it gets the
|
|
95
|
+
# secrets of the CHANNEL's profile, never whatever happens to sit in
|
|
96
|
+
# this shell. Agents that can't see that list assume the worst and
|
|
97
|
+
# hardcode the value into the check script — printing it is the fix.
|
|
98
|
+
ch = next((c for c in api("GET", "/channels") if c.get("sessionId") == SID), None)
|
|
99
|
+
if not ch:
|
|
100
|
+
print("this channel is not registered server-side: a guard would run from the")
|
|
101
|
+
print("server's own directory, with no profile secrets at all.")
|
|
102
|
+
return
|
|
103
|
+
print(f"guard cwd: {ch.get('cwd') or '(server default)'}")
|
|
104
|
+
pname = ch.get("profile")
|
|
105
|
+
if not pname:
|
|
106
|
+
print("guard secrets: NONE — this channel has no profile.")
|
|
107
|
+
print(" Secrets reach a guard only through the channel's profile; attach one")
|
|
108
|
+
print(" (web UI, the agent's profile picker) that lists the names you need.")
|
|
109
|
+
return
|
|
110
|
+
prof = next((p for p in api("GET", "/profiles") if p.get("name") == pname), None)
|
|
111
|
+
if prof is None:
|
|
112
|
+
# Same end result as "no secrets", but a very different cause: say
|
|
113
|
+
# which one, or the report reads as a working setup.
|
|
114
|
+
print(f"guard secrets: NONE — the channel points at profile '{pname}', which no longer exists.")
|
|
115
|
+
return
|
|
116
|
+
wanted = prof.get("secrets") or []
|
|
117
|
+
if not wanted:
|
|
118
|
+
print(f"guard secrets: NONE — profile '{pname}' lists no secret.")
|
|
119
|
+
return
|
|
120
|
+
vault = set(api("GET", "/secrets").get("names") or [])
|
|
121
|
+
present = [n for n in wanted if n in vault]
|
|
122
|
+
missing = [n for n in wanted if n not in vault]
|
|
123
|
+
print(f"guard secrets (profile '{pname}'): {', '.join(present) if present else 'NONE'}")
|
|
124
|
+
if missing:
|
|
125
|
+
# `secretsFor` skips an unknown name without a word, so a typo here
|
|
126
|
+
# looks exactly like a working guard until the day it runs.
|
|
127
|
+
print(f" referenced but NOT in the vault, so absent at run time: {', '.join(missing)}")
|
|
92
128
|
elif args.cmd == "del":
|
|
93
129
|
# `list` n'affiche que 8 caractères de l'id : le serveur accepte ce
|
|
94
130
|
# préfixe. On imprime CE QU'IL a supprimé — l'ancienne version
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1227,6 +1227,10 @@
|
|
|
1227
1227
|
<div id="profileGrid" role="radiogroup" aria-label="Agent profile"></div>
|
|
1228
1228
|
<span class="check-hint" id="profileNaNote" hidden>Applies to new sessions only.</span>
|
|
1229
1229
|
</div>
|
|
1230
|
+
<div class="field">
|
|
1231
|
+
<span class="label">Name</span>
|
|
1232
|
+
<input type="text" id="nameInput" placeholder="agent" autocomplete="off" spellcheck="false">
|
|
1233
|
+
</div>
|
|
1230
1234
|
<div class="field">
|
|
1231
1235
|
<span class="label">Working directory</span>
|
|
1232
1236
|
<input type="text" id="cwdInput" placeholder="/path/to/project">
|
|
@@ -1463,12 +1467,13 @@
|
|
|
1463
1467
|
qui rend inoffensif le HTML qu'un agent pourrait écrire dans le transcript. -->
|
|
1464
1468
|
<script type="module" nonce="__CSP_NONCE__">
|
|
1465
1469
|
import { extractLiveText } from "/live-text.js";
|
|
1466
|
-
import { profileBlurb, profileBadges } from "/profile-card.js";
|
|
1470
|
+
import { profileBlurb, profileBadges, defaultAgentName } from "/profile-card.js";
|
|
1467
1471
|
import { notifyState, BLINK_MS } from "/notify.js";
|
|
1468
1472
|
import { dialPos, dialColor, arcSegments, dialTitle, SWEEP_DEG } from "/gauge-dial.js";
|
|
1469
1473
|
window.extractLiveText = extractLiveText;
|
|
1470
1474
|
window.profileBlurb = profileBlurb;
|
|
1471
1475
|
window.profileBadges = profileBadges;
|
|
1476
|
+
window.defaultAgentName = defaultAgentName;
|
|
1472
1477
|
window.notifyState = notifyState;
|
|
1473
1478
|
window.BLINK_MS = BLINK_MS;
|
|
1474
1479
|
window.dialPos = dialPos;
|
|
@@ -3228,6 +3233,10 @@
|
|
|
3228
3233
|
// écraserait le choix en cours de saisie.
|
|
3229
3234
|
profileTouched = false;
|
|
3230
3235
|
applyRememberedProfile();
|
|
3236
|
+
// Même règle que le profil : on ne repropose le défaut qu'à l'OUVERTURE,
|
|
3237
|
+
// pour ne pas écraser un nom en cours de frappe.
|
|
3238
|
+
nameTouched = false;
|
|
3239
|
+
refreshDefaultName();
|
|
3231
3240
|
refreshRecoverList();
|
|
3232
3241
|
refreshLiveList();
|
|
3233
3242
|
$("setupOverlay").hidden = false;
|
|
@@ -3256,7 +3265,13 @@
|
|
|
3256
3265
|
$("startBtn").disabled = true;
|
|
3257
3266
|
closeSetup();
|
|
3258
3267
|
// createTab active l'onglet créé ; launchTab ouvre le lien.
|
|
3259
|
-
|
|
3268
|
+
const t = createTab();
|
|
3269
|
+
// Le nom est posé AVANT launchTab, qui sinon écrase avec basename(cwd).
|
|
3270
|
+
// customName le protège aussi du `ready` et de la restauration des canaux.
|
|
3271
|
+
const wanted = $("nameInput").value.trim() || defaultAgentNameNow();
|
|
3272
|
+
t.customName = true;
|
|
3273
|
+
t.nameEl.textContent = wanted;
|
|
3274
|
+
launchTab(t, msg);
|
|
3260
3275
|
}
|
|
3261
3276
|
|
|
3262
3277
|
// ── Version display + auto-reload ─────────────────────────────────────────
|
|
@@ -3685,11 +3700,16 @@
|
|
|
3685
3700
|
if (mode === "resume") refreshSessionList();
|
|
3686
3701
|
})
|
|
3687
3702
|
);
|
|
3703
|
+
$("nameInput").addEventListener("input", () => {
|
|
3704
|
+
nameTouched = $("nameInput").value.trim() !== "";
|
|
3705
|
+
});
|
|
3688
3706
|
$("cwdInput").addEventListener("change", () => {
|
|
3689
3707
|
if (!$("resumeField").hidden) refreshSessionList();
|
|
3690
3708
|
refreshRecoverList();
|
|
3691
3709
|
// Un autre dossier a peut-être son propre habitué — sauf si on a déjà choisi.
|
|
3692
3710
|
if (!profileTouched) applyRememberedProfile();
|
|
3711
|
+
// Sans profil, le défaut EST le dossier : il doit suivre la saisie.
|
|
3712
|
+
refreshDefaultName();
|
|
3693
3713
|
});
|
|
3694
3714
|
// Agents can appear while the setup screen sits open (pilotctl spawns one,
|
|
3695
3715
|
// another tab starts one) — keep the list current without a reload.
|
|
@@ -4247,6 +4267,9 @@
|
|
|
4247
4267
|
selectedProfile = name || "";
|
|
4248
4268
|
if (byUser) profileTouched = true;
|
|
4249
4269
|
syncProfileSelection();
|
|
4270
|
+
// Le nom par défaut DÉRIVE du profil : changer de carte doit le reproposer,
|
|
4271
|
+
// sinon on lance un « Shadok-dev » qui tourne en fait sur Shadok-Support.
|
|
4272
|
+
refreshDefaultName();
|
|
4250
4273
|
}
|
|
4251
4274
|
|
|
4252
4275
|
function syncProfileSelection() {
|
|
@@ -4284,6 +4307,23 @@
|
|
|
4284
4307
|
selectProfile(remembered); // syncProfileSelection retombe sur "" s'il n'existe plus
|
|
4285
4308
|
}
|
|
4286
4309
|
|
|
4310
|
+
// ── Nom par défaut d'un nouvel agent ─────────────────────────────────────
|
|
4311
|
+
let nameTouched = false; // l'utilisateur a tapé le sien → on n'écrase plus
|
|
4312
|
+
|
|
4313
|
+
/** Le défaut courant, d'après le profil sélectionné et le dossier saisi. */
|
|
4314
|
+
function defaultAgentNameNow() {
|
|
4315
|
+
// Garde alignée sur les autres usages du pont ESM (cf. invariant 10) : si le
|
|
4316
|
+
// module n'est pas encore là, un nom vide vaut mieux qu'une exception.
|
|
4317
|
+
return window.defaultAgentName ? window.defaultAgentName(selectedProfile, $("cwdInput").value) : "";
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
/** Repose le défaut dans le champ, sauf si l'utilisateur l'a déjà écrit. */
|
|
4321
|
+
function refreshDefaultName() {
|
|
4322
|
+
if (nameTouched) return;
|
|
4323
|
+
const el = $("nameInput");
|
|
4324
|
+
if (el) el.value = defaultAgentNameNow();
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4287
4327
|
/** Flèches = déplacer la sélection (convention radiogroup). Espace/Entrée
|
|
4288
4328
|
* sont déjà le clic natif du <button>. */
|
|
4289
4329
|
function onProfileKey(e, onPick) {
|
package/public/profile-card.js
CHANGED
|
@@ -63,3 +63,18 @@ export function profileBadges(profile) {
|
|
|
63
63
|
});
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Le nom proposé pour un nouvel agent : celui de son PROFIL, parce que c'est ce
|
|
69
|
+
* qui distingue deux agents lancés sur le même dépôt — le dossier, lui, est le
|
|
70
|
+
* même pour tous et donnait des colonnes entières d'onglets homonymes.
|
|
71
|
+
* Sans profil, on retombe sur le nom du dossier ; en dernier recours "agent",
|
|
72
|
+
* jamais une chaîne vide (un onglet sans nom est illisible).
|
|
73
|
+
*/
|
|
74
|
+
export function defaultAgentName(profileName, cwd) {
|
|
75
|
+
const p = String(profileName ?? "").trim();
|
|
76
|
+
if (p) return p;
|
|
77
|
+
const dir = String(cwd ?? "").trim().replace(/[/\\]+$/, "");
|
|
78
|
+
const base = dir.split(/[/\\]/).pop() || "";
|
|
79
|
+
return base || "agent";
|
|
80
|
+
}
|