Spaces:
Paused
Paused
Only hf-cli on by default; the user's skill choices kept across machines
Browse files- Dockerfile +4 -0
- piweb/hf-design.patch +224 -0
- sandbox/AGENTS.md +1 -1
- sandbox/skill-defaults.py +18 -0
Dockerfile
CHANGED
|
@@ -23,6 +23,10 @@ RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --p
|
|
| 23 |
&& for skill in $(hf skills list -q 2>/dev/null | grep -vx 'hf-cli' | grep -Ev "${HF_SKILLS_EXCLUDE}"); do hf skills add "$skill" --global --force; done \
|
| 24 |
&& rm -rf /root/.claude /root/.cache/huggingface \
|
| 25 |
&& ls /root/.agents/skills
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# A real Chromium for Pi Web's Browser pane and the agent's browser_* tools (lib/browser): the
|
| 28 |
# revision playwright-core 1.63.0 drives (Pi Web's own dependency), with the system libraries
|
|
|
|
| 23 |
&& for skill in $(hf skills list -q 2>/dev/null | grep -vx 'hf-cli' | grep -Ev "${HF_SKILLS_EXCLUDE}"); do hf skills add "$skill" --global --force; done \
|
| 24 |
&& rm -rf /root/.claude /root/.cache/huggingface \
|
| 25 |
&& ls /root/.agents/skills
|
| 26 |
+
# Only hf-cli is on by default; the other skills stay installed but off (sandbox/skill-defaults.py).
|
| 27 |
+
ARG HF_SKILLS_ENABLED="hf-cli"
|
| 28 |
+
COPY sandbox/skill-defaults.py /tmp/skill-defaults.py
|
| 29 |
+
RUN python3 /tmp/skill-defaults.py /root/.agents/skills ${HF_SKILLS_ENABLED} && rm /tmp/skill-defaults.py
|
| 30 |
|
| 31 |
# A real Chromium for Pi Web's Browser pane and the agent's browser_* tools (lib/browser): the
|
| 32 |
# revision playwright-core 1.63.0 drives (Pi Web's own dependency), with the system libraries
|
piweb/hf-design.patch
CHANGED
|
@@ -468,6 +468,27 @@ index 0000000..0e30af4
|
|
| 468 |
+export const PATCH = handle;
|
| 469 |
+export const DELETE = handle;
|
| 470 |
+export const OPTIONS = handle;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
diff --git a/app/api/terminal/[id]/events/route.ts b/app/api/terminal/[id]/events/route.ts
|
| 472 |
index 92511c1..baeec2f 100644
|
| 473 |
--- a/app/api/terminal/[id]/events/route.ts
|
|
@@ -32411,6 +32432,25 @@ index 38b6356..0000000
|
|
| 32411 |
- }, [enabled, ready]);
|
| 32412 |
- return ready;
|
| 32413 |
-}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32414 |
diff --git a/lib/browser/event-channel.test.mjs b/lib/browser/event-channel.test.mjs
|
| 32415 |
new file mode 100644
|
| 32416 |
index 0000000..90b7506
|
|
@@ -39559,6 +39599,190 @@ index 0000000..2f7cd39
|
|
| 39559 |
+ const ids: ShortcutId[] = ["terminal", "changes", "browser", "files", "expandPane", ...(mac ? ["closePane" as const] : [])];
|
| 39560 |
+ return ids.some((id) => isShortcut(event, id, mac));
|
| 39561 |
+}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39562 |
diff --git a/lib/step-summary.test.mjs b/lib/step-summary.test.mjs
|
| 39563 |
new file mode 100644
|
| 39564 |
index 0000000..6cbe005
|
|
|
|
| 468 |
+export const PATCH = handle;
|
| 469 |
+export const DELETE = handle;
|
| 470 |
+export const OPTIONS = handle;
|
| 471 |
+
diff --git a/app/api/skills/route.ts b/app/api/skills/route.ts
|
| 472 |
+
index a503398..2feacc5 100644
|
| 473 |
+
--- a/app/api/skills/route.ts
|
| 474 |
+
+++ b/app/api/skills/route.ts
|
| 475 |
+
@@ -6,6 +6,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
| 476 |
+
import type { SkillToggleResult } from "@/lib/api-types";
|
| 477 |
+
import { loadSkillsWithInstallInfo } from "@/lib/skills-service";
|
| 478 |
+
import { setDisableModelInvocation } from "@/lib/skill-frontmatter";
|
| 479 |
+
+import { recordSkillChoice } from "@/lib/skill-choices";
|
| 480 |
+
import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access";
|
| 481 |
+
|
| 482 |
+
export const dynamic = "force-dynamic";
|
| 483 |
+
@@ -58,6 +59,8 @@ function toggleSkillFile(
|
| 484 |
+
const updated = setDisableModelInvocation(content, disableModelInvocation);
|
| 485 |
+
// A skill already in the requested state keeps its file and mtime.
|
| 486 |
+
if (updated !== content) writeFileSync(filePath, updated, "utf8");
|
| 487 |
+
+ // An image's skill comes back with the image's default on a new machine: keep the choice.
|
| 488 |
+
+ recordSkillChoice(filePath, !disableModelInvocation);
|
| 489 |
+
return null;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
diff --git a/app/api/terminal/[id]/events/route.ts b/app/api/terminal/[id]/events/route.ts
|
| 493 |
index 92511c1..baeec2f 100644
|
| 494 |
--- a/app/api/terminal/[id]/events/route.ts
|
|
|
|
| 32432 |
- }, [enabled, ready]);
|
| 32433 |
- return ready;
|
| 32434 |
-}
|
| 32435 |
+
diff --git a/instrumentation-node.ts b/instrumentation-node.ts
|
| 32436 |
+
index cb2f025..2abf76e 100644
|
| 32437 |
+
--- a/instrumentation-node.ts
|
| 32438 |
+
+++ b/instrumentation-node.ts
|
| 32439 |
+
@@ -21,4 +21,14 @@ export function registerNodeInstrumentation(): void {
|
| 32440 |
+
// it overlaps the browser opening, and the routes reuse Node's cached module.
|
| 32441 |
+
// A failure is left for the routes' own import to report.
|
| 32442 |
+
import("@earendil-works/pi-coding-agent").catch(() => {});
|
| 32443 |
+
+
|
| 32444 |
+
+ // The skills a machine image installs come back with the image's defaults on a
|
| 32445 |
+
+ // new machine: put the user's own choices back before any session loads them.
|
| 32446 |
+
+ import("@/lib/skill-choices")
|
| 32447 |
+
+ .then(({ applySkillChoices }) => {
|
| 32448 |
+
+ const { applied, failed } = applySkillChoices();
|
| 32449 |
+
+ if (applied.length) console.log(`[skills] kept choices applied: ${applied.join(", ")}`);
|
| 32450 |
+
+ if (failed.length) console.warn(`[skills] could not apply the kept choice for: ${failed.join(", ")}`);
|
| 32451 |
+
+ })
|
| 32452 |
+
+ .catch(() => {});
|
| 32453 |
+
}
|
| 32454 |
diff --git a/lib/browser/event-channel.test.mjs b/lib/browser/event-channel.test.mjs
|
| 32455 |
new file mode 100644
|
| 32456 |
index 0000000..90b7506
|
|
|
|
| 39599 |
+ const ids: ShortcutId[] = ["terminal", "changes", "browser", "files", "expandPane", ...(mac ? ["closePane" as const] : [])];
|
| 39600 |
+ return ids.some((id) => isShortcut(event, id, mac));
|
| 39601 |
+}
|
| 39602 |
+
diff --git a/lib/skill-choices.test.mjs b/lib/skill-choices.test.mjs
|
| 39603 |
+
new file mode 100644
|
| 39604 |
+
index 0000000..fa03ef3
|
| 39605 |
+
--- /dev/null
|
| 39606 |
+
+++ b/lib/skill-choices.test.mjs
|
| 39607 |
+
@@ -0,0 +1,74 @@
|
| 39608 |
+
+import assert from "node:assert/strict";
|
| 39609 |
+
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
| 39610 |
+
+import { tmpdir } from "node:os";
|
| 39611 |
+
+import path from "node:path";
|
| 39612 |
+
+import test from "node:test";
|
| 39613 |
+
+import { createJiti } from "jiti";
|
| 39614 |
+
+
|
| 39615 |
+
+const jiti = createJiti(import.meta.url, { alias: { "@": process.cwd() }, interopDefault: true });
|
| 39616 |
+
+const { applySkillChoices, globalSkillNameOf, readSkillChoices, recordSkillChoice } = await jiti.import("./skill-choices.ts");
|
| 39617 |
+
+
|
| 39618 |
+
+function setup(t) {
|
| 39619 |
+
+ const dir = mkdtempSync(path.join(tmpdir(), "pi-web-skill-choices-"));
|
| 39620 |
+
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
|
| 39621 |
+
+ const root = path.join(dir, "home", ".agents", "skills");
|
| 39622 |
+
+ const file = path.join(dir, "agent", "pi-web", "skill-choices.json");
|
| 39623 |
+
+ const skill = (name, disabled) => {
|
| 39624 |
+
+ mkdirSync(path.join(root, name), { recursive: true });
|
| 39625 |
+
+ const p = path.join(root, name, "SKILL.md");
|
| 39626 |
+
+ writeFileSync(p, `---\nname: ${name}\n${disabled ? "disable-model-invocation: true\n" : ""}description: ${name} skill\n---\n\n# ${name}\n`);
|
| 39627 |
+
+ return p;
|
| 39628 |
+
+ };
|
| 39629 |
+
+ return { dir, root, file, skill };
|
| 39630 |
+
+}
|
| 39631 |
+
+
|
| 39632 |
+
+const disabled = (p) => /disable-model-invocation: true/.test(readFileSync(p, "utf8"));
|
| 39633 |
+
+
|
| 39634 |
+
+test("only a skill under the global root gets a kept choice, symlinked or not", (t) => {
|
| 39635 |
+
+ const { dir, root, file, skill } = setup(t);
|
| 39636 |
+
+ const cli = skill("hf-cli", false);
|
| 39637 |
+
+ assert.equal(globalSkillNameOf(cli, root), "hf-cli");
|
| 39638 |
+
+ const link = path.join(dir, "agent", "skills", "hf-cli");
|
| 39639 |
+
+ mkdirSync(path.dirname(link), { recursive: true });
|
| 39640 |
+
+ symlinkSync(path.join(root, "hf-cli"), link);
|
| 39641 |
+
+ assert.equal(globalSkillNameOf(path.join(link, "SKILL.md"), root), "hf-cli");
|
| 39642 |
+
+ const own = path.join(dir, "agent", "skills", "mine", "SKILL.md");
|
| 39643 |
+
+ mkdirSync(path.dirname(own), { recursive: true });
|
| 39644 |
+
+ writeFileSync(own, "---\nname: mine\n---\n");
|
| 39645 |
+
+ assert.equal(globalSkillNameOf(own, root), null);
|
| 39646 |
+
+ recordSkillChoice(own, false, { root, file });
|
| 39647 |
+
+ assert.deepEqual(readSkillChoices(file).skills, {});
|
| 39648 |
+
+ assert.equal(globalSkillNameOf(path.join(root, "..", "elsewhere.md"), root), null);
|
| 39649 |
+
+});
|
| 39650 |
+
+
|
| 39651 |
+
+test("choices are kept and applied over the image's defaults on a new machine", (t) => {
|
| 39652 |
+
+ const { root, file, skill } = setup(t);
|
| 39653 |
+
+ skill("hf-cli", false);
|
| 39654 |
+
+ const datasets = skill("huggingface-datasets", true);
|
| 39655 |
+
+ recordSkillChoice(path.join(root, "hf-cli", "SKILL.md"), false, { root, file });
|
| 39656 |
+
+ recordSkillChoice(datasets, true, { root, file });
|
| 39657 |
+
+ assert.deepEqual(readSkillChoices(file).skills, { "hf-cli": false, "huggingface-datasets": true });
|
| 39658 |
+
+
|
| 39659 |
+
+ // A new machine: the image's files again (hf-cli on, the rest off), a skill the user never touched, and one gone.
|
| 39660 |
+
+ const cli = skill("hf-cli", false);
|
| 39661 |
+
+ skill("huggingface-datasets", true);
|
| 39662 |
+
+ const gradio = skill("huggingface-gradio", true);
|
| 39663 |
+
+ recordSkillChoice(path.join(root, "huggingface-datasets", "SKILL.md"), true, { root, file });
|
| 39664 |
+
+ writeFileSync(file, JSON.stringify({ version: 1, skills: { ...readSkillChoices(file).skills, removed: true, "../x": true } }));
|
| 39665 |
+
+ const result = applySkillChoices({ root, file });
|
| 39666 |
+
+ assert.deepEqual(result.applied.sort(), ["hf-cli", "huggingface-datasets"]);
|
| 39667 |
+
+ assert.deepEqual(result.failed, []);
|
| 39668 |
+
+ assert.equal(disabled(cli), true);
|
| 39669 |
+
+ assert.equal(disabled(path.join(root, "huggingface-datasets", "SKILL.md")), false);
|
| 39670 |
+
+ assert.equal(disabled(gradio), true, "an untouched skill keeps the image's default");
|
| 39671 |
+
+ // Applying again changes nothing.
|
| 39672 |
+
+ assert.deepEqual(applySkillChoices({ root, file }).applied, []);
|
| 39673 |
+
+});
|
| 39674 |
+
+
|
| 39675 |
+
+test("a missing or broken choices file means no choices", (t) => {
|
| 39676 |
+
+ const { file } = setup(t);
|
| 39677 |
+
+ assert.deepEqual(readSkillChoices(file).skills, {});
|
| 39678 |
+
+ mkdirSync(path.dirname(file), { recursive: true });
|
| 39679 |
+
+ writeFileSync(file, "{not json");
|
| 39680 |
+
+ assert.deepEqual(readSkillChoices(file).skills, {});
|
| 39681 |
+
+});
|
| 39682 |
+
diff --git a/lib/skill-choices.ts b/lib/skill-choices.ts
|
| 39683 |
+
new file mode 100644
|
| 39684 |
+
index 0000000..51e8cd0
|
| 39685 |
+
--- /dev/null
|
| 39686 |
+
+++ b/lib/skill-choices.ts
|
| 39687 |
+
@@ -0,0 +1,98 @@
|
| 39688 |
+
+import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
| 39689 |
+
+import { homedir } from "os";
|
| 39690 |
+
+import path from "path";
|
| 39691 |
+
+import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
| 39692 |
+
+import { setDisableModelInvocation } from "./skill-frontmatter";
|
| 39693 |
+
+
|
| 39694 |
+
+// A skill's on/off state is `disable-model-invocation` in its own SKILL.md. The
|
| 39695 |
+
+// skills a machine image installs in ~/.agents/skills come back from the image
|
| 39696 |
+
+// on every new machine, with the image's defaults, so an edit there alone is
|
| 39697 |
+
+// lost. The user's explicit choices for them are also kept in the agent dir,
|
| 39698 |
+
+// which a workspace keeps, and applied again when Pi Web starts.
|
| 39699 |
+
+
|
| 39700 |
+
+interface SkillChoices {
|
| 39701 |
+
+ version: 1;
|
| 39702 |
+
+ /** Skill directory name under ~/.agents/skills -> enabled. */
|
| 39703 |
+
+ skills: Record<string, boolean>;
|
| 39704 |
+
+}
|
| 39705 |
+
+
|
| 39706 |
+
+export function globalSkillsRoot(home = homedir()): string {
|
| 39707 |
+
+ return path.join(home, ".agents", "skills");
|
| 39708 |
+
+}
|
| 39709 |
+
+
|
| 39710 |
+
+export function skillChoicesFile(agentDir = getAgentDir()): string {
|
| 39711 |
+
+ return path.join(agentDir, "pi-web", "skill-choices.json");
|
| 39712 |
+
+}
|
| 39713 |
+
+
|
| 39714 |
+
+function real(target: string): string | null {
|
| 39715 |
+
+ try {
|
| 39716 |
+
+ return realpathSync(target);
|
| 39717 |
+
+ } catch {
|
| 39718 |
+
+ return null;
|
| 39719 |
+
+ }
|
| 39720 |
+
+}
|
| 39721 |
+
+
|
| 39722 |
+
+/** The skill's directory name when `filePath` is a skill under the global root (a symlink to one too). */
|
| 39723 |
+
+export function globalSkillNameOf(filePath: string, root = globalSkillsRoot()): string | null {
|
| 39724 |
+
+ const file = real(filePath);
|
| 39725 |
+
+ const base = real(root);
|
| 39726 |
+
+ if (!file || !base) return null;
|
| 39727 |
+
+ const relative = path.relative(base, file);
|
| 39728 |
+
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
| 39729 |
+
+ const [name, ...rest] = relative.split(path.sep);
|
| 39730 |
+
+ return rest.length ? name : null;
|
| 39731 |
+
+}
|
| 39732 |
+
+
|
| 39733 |
+
+export function readSkillChoices(file = skillChoicesFile()): SkillChoices {
|
| 39734 |
+
+ try {
|
| 39735 |
+
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<SkillChoices>;
|
| 39736 |
+
+ const skills: Record<string, boolean> = {};
|
| 39737 |
+
+ for (const [name, enabled] of Object.entries(parsed.skills ?? {})) {
|
| 39738 |
+
+ if (typeof enabled === "boolean" && /^[\w.-]+$/.test(name)) skills[name] = enabled;
|
| 39739 |
+
+ }
|
| 39740 |
+
+ return { version: 1, skills };
|
| 39741 |
+
+ } catch {
|
| 39742 |
+
+ return { version: 1, skills: {} };
|
| 39743 |
+
+ }
|
| 39744 |
+
+}
|
| 39745 |
+
+
|
| 39746 |
+
+/** Keeps the user's choice for a global skill they just toggled; other skills keep theirs in their own files. */
|
| 39747 |
+
+export function recordSkillChoice(
|
| 39748 |
+
+ filePath: string,
|
| 39749 |
+
+ enabled: boolean,
|
| 39750 |
+
+ options: { root?: string; file?: string } = {},
|
| 39751 |
+
+): void {
|
| 39752 |
+
+ const name = globalSkillNameOf(filePath, options.root);
|
| 39753 |
+
+ if (!name) return;
|
| 39754 |
+
+ const file = options.file ?? skillChoicesFile();
|
| 39755 |
+
+ const choices = readSkillChoices(file);
|
| 39756 |
+
+ if (choices.skills[name] === enabled) return;
|
| 39757 |
+
+ choices.skills[name] = enabled;
|
| 39758 |
+
+ mkdirSync(path.dirname(file), { recursive: true });
|
| 39759 |
+
+ const tmp = `${file}.${process.pid}.tmp`;
|
| 39760 |
+
+ writeFileSync(tmp, `${JSON.stringify(choices, null, 2)}\n`, "utf8");
|
| 39761 |
+
+ renameSync(tmp, file);
|
| 39762 |
+
+}
|
| 39763 |
+
+
|
| 39764 |
+
+/** Applies the kept choices to the global skills present; a skill no longer installed is left in the file. */
|
| 39765 |
+
+export function applySkillChoices(options: { root?: string; file?: string } = {}): { applied: string[]; failed: string[] } {
|
| 39766 |
+
+ const root = options.root ?? globalSkillsRoot();
|
| 39767 |
+
+ const { skills } = readSkillChoices(options.file);
|
| 39768 |
+
+ const applied: string[] = [];
|
| 39769 |
+
+ const failed: string[] = [];
|
| 39770 |
+
+ for (const [name, enabled] of Object.entries(skills)) {
|
| 39771 |
+
+ const skillFile = path.join(root, name, "SKILL.md");
|
| 39772 |
+
+ if (!existsSync(skillFile)) continue;
|
| 39773 |
+
+ try {
|
| 39774 |
+
+ const content = readFileSync(skillFile, "utf8");
|
| 39775 |
+
+ const updated = setDisableModelInvocation(content, !enabled);
|
| 39776 |
+
+ if (updated !== content) {
|
| 39777 |
+
+ writeFileSync(skillFile, updated, "utf8");
|
| 39778 |
+
+ applied.push(name);
|
| 39779 |
+
+ }
|
| 39780 |
+
+ } catch {
|
| 39781 |
+
+ failed.push(name);
|
| 39782 |
+
+ }
|
| 39783 |
+
+ }
|
| 39784 |
+
+ return { applied, failed };
|
| 39785 |
+
+}
|
| 39786 |
diff --git a/lib/step-summary.test.mjs b/lib/step-summary.test.mjs
|
| 39787 |
new file mode 100644
|
| 39788 |
index 0000000..6cbe005
|
sandbox/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ You run inside a disposable Hugging Face Sandbox (a container on HF Jobs, in the
|
|
| 11 |
|
| 12 |
## Hugging Face
|
| 13 |
|
| 14 |
-
- The Hugging Face CLI `hf` is installed,
|
| 15 |
- The Space lends the token of the user's sign-in to this machine: `~/.cache/huggingface/token` holds their scoped, expiring token, so `hf`, `huggingface_hub` and `git` (clone and push on huggingface.co) act as that user, within the permissions they granted. Check with `hf auth whoami`. The token is renewed in place; read the file when you need it rather than copying it.
|
| 16 |
- If there is no token, the user has taken it back or their sign-in lapsed: do the rest of the work, then tell them to choose *Sign in again* or *Lend my token* in the Workspace menu (bottom of the sidebar), after which the token appears within seconds. Do not run `hf auth login` and never ask for a token in the chat.
|
| 17 |
- Never print, log, copy or commit the token. For tools that only read `HF_TOKEN`, pass it for that command: `HF_TOKEN=$(cat ~/.cache/huggingface/token) node script.mjs`.
|
|
|
|
| 11 |
|
| 12 |
## Hugging Face
|
| 13 |
|
| 14 |
+
- The Hugging Face CLI `hf` is installed, with its skill `hf-cli`: use it for anything on the Hub. More Hugging Face skills (Spaces, datasets, training, Gradio, ...) are installed but off unless the user turned them on in Settings > Skills; the ones that are on are among your skills.
|
| 15 |
- The Space lends the token of the user's sign-in to this machine: `~/.cache/huggingface/token` holds their scoped, expiring token, so `hf`, `huggingface_hub` and `git` (clone and push on huggingface.co) act as that user, within the permissions they granted. Check with `hf auth whoami`. The token is renewed in place; read the file when you need it rather than copying it.
|
| 16 |
- If there is no token, the user has taken it back or their sign-in lapsed: do the rest of the work, then tell them to choose *Sign in again* or *Lend my token* in the Workspace menu (bottom of the sidebar), after which the token appears within seconds. Do not run `hf auth login` and never ask for a token in the chat.
|
| 17 |
- Never print, log, copy or commit the token. For tools that only read `HF_TOKEN`, pass it for that command: `HF_TOKEN=$(cat ~/.cache/huggingface/token) node script.mjs`.
|
sandbox/skill-defaults.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""skill-defaults.py <skills dir> <enabled skill>...: the image's skill defaults. The listed skills are on,
|
| 2 |
+
every other one is off (`disable-model-invocation: true`: not offered to the model, still usable as
|
| 3 |
+
/skill:<name>, and turned on in Pi Web's Settings > Skills). Pi Web keeps a user's own choices in the agent
|
| 4 |
+
dir and puts them back on every new machine (lib/skill-choices.ts)."""
|
| 5 |
+
import pathlib, re, sys
|
| 6 |
+
|
| 7 |
+
root, enabled = pathlib.Path(sys.argv[1]), set(sys.argv[2:])
|
| 8 |
+
skills = sorted(root.glob("*/SKILL.md"))
|
| 9 |
+
assert any(p.parent.name in enabled for p in skills), f"none of {sorted(enabled)} in {root}"
|
| 10 |
+
for skill in skills:
|
| 11 |
+
name, text = skill.parent.name, skill.read_text()
|
| 12 |
+
head = re.match(r"\A---[ \t]*\n(.*?\n)---", text, re.S)
|
| 13 |
+
assert head, f"{name}: no frontmatter"
|
| 14 |
+
body = re.sub(r"(?m)^disable-model-invocation:.*\n", "", head.group(1))
|
| 15 |
+
if name not in enabled:
|
| 16 |
+
body += "disable-model-invocation: true\n"
|
| 17 |
+
skill.write_text(text[: head.start(1)] + body + text[head.end(1):])
|
| 18 |
+
print("on " if name in enabled else "off", name)
|