feat(core): reserved-port and API-key guardrails in the default system prompt (#24)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-22 22:04:30 +08:00
committed by GitHub
parent 849f2fa6d2
commit c9cd1f9dc5
10 changed files with 47 additions and 5 deletions
+3 -2
View File
@@ -14,11 +14,12 @@
* Docs: /docs/cli § "penguin server / penguin web".
*/
import { spawn } from "node:child_process";
import { DEFAULT_SERVER_PORT } from "@prismshadow/penguin-core";
import type { Command } from "commander";
import type { Messages } from "../i18n.js";
/** Default service port (deliberately avoids common defaults like 3000/8080). */
export const DEFAULT_PORT = 7364;
/** Default service port — core's DEFAULT_SERVER_PORT (7364), the single source of truth. */
export const DEFAULT_PORT = DEFAULT_SERVER_PORT;
/** Default service listen host. */
export const DEFAULT_HOST = "127.0.0.1";
+4
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { Command } from "commander";
import { DEFAULT_SERVER_PORT } from "@prismshadow/penguin-core";
import {
DEFAULT_HOST,
DEFAULT_PORT,
@@ -11,6 +12,9 @@ import {
import { getMessages } from "../src/i18n.js";
describe("resolvePort (option > env var > default 7364)", () => {
it("derives DEFAULT_PORT from core's DEFAULT_SERVER_PORT", () => {
expect(DEFAULT_PORT).toBe(DEFAULT_SERVER_PORT);
});
it("uses the default 7364 when neither is given", () => {
expect(DEFAULT_PORT).toBe(7364);
expect(resolvePort(undefined, undefined)).toBe(7364);
+3
View File
@@ -19,6 +19,9 @@
export * from "./omnimessage/index.js";
export * from "./interfaces.js";
// Only the default server port leaves internal: the CLI / server default-port source of truth.
export { DEFAULT_SERVER_PORT } from "./internal/ports.js";
// Submodules
export * from "./state/index.js";
export * from "./llm/index.js";
+10
View File
@@ -0,0 +1,10 @@
/**
* Default PenguinHarness server port (internal shared constant; the barrel re-exports
* only DEFAULT_SERVER_PORT, as the CLI `penguin server` / `penguin web` and server
* default-port source of truth — previously each hardcoded the number). It is a
* fallback only: the `--port` flag and the PORT environment variable override it at
* runtime.
*/
/** Default main server / Web UI port; deliberately avoids common defaults like 3000/8080. */
export const DEFAULT_SERVER_PORT = 7364;
+3 -1
View File
@@ -92,13 +92,15 @@ Communicate with the user precisely and concisely, yet with warmth. Do not repea
# Constraints
- Make the smallest change that satisfies the request; do not modify unrelated files.
- Destructive operations are forbidden.
- Never kill a process you did not start yourself (e.g. to free a busy port) unless the user explicitly asks you to.
- Never kill a process you did not start yourself unless the user explicitly asks you to, including PenguinHarness's own services. Never take a PenguinHarness service port for your own servers; when a port you want is busy, pick another free port instead of killing the listener.
- If a tool call fails, read the error, adjust, and retry; never repeat the same failing input.
- On an API authentication/authorization or API-key error (401/403, invalid or missing key), retry at most once.
# Stop rules
- Stop and give the final answer once the success criteria are met.
- If the request is ambiguous, stop and ask the user for clarification instead of guessing their intent.
- If you hit an error you cannot resolve, stop and report the blocker to the user.
- If an API auth/key error persists after that one retry, stop calling tools and ask the user to update the key in the agent's vault or the model settings outside the chat — the secret value must never be pasted into the conversation. Updated secrets only take effect in the next conversation, so further retries cannot succeed.
# Tool use
- Prefer solving problems with your tools: inspect the real files and environment and run real commands instead of answering from memory or guessing.
+16
View File
@@ -315,6 +315,22 @@ describe("assembleSystemPrompt", () => {
expect(prompt).not.toContain(DATE_PLACEHOLDER);
});
it("default prompt carries the port and API-key guardrails", async () => {
const state = await loadOrInitAgentState();
const prompt = assembleSystemPrompt(state);
// Ports: never kill listeners or take PenguinHarness service ports; numbers are deliberately not listed.
expect(prompt).toContain("pick another free port");
expect(prompt).toContain("PenguinHarness service port");
expect(prompt).not.toContain("7364");
// Auth/key failures: retry at most once (Constraints), then stop and ask the user to update
// the key outside the chat (Stop rules) — no CLI commands, no secret values in the conversation.
expect(prompt).toContain("retry at most once");
expect(prompt).toContain("stop calling tools");
expect(prompt).toContain("never be pasted into the conversation");
expect(prompt).toContain("next conversation");
expect(prompt).not.toContain("penguin config vault set");
});
it("replaces AGENTS.md and specific Session environment fields at template locations", () => {
const state = {
root: tmpRoot,
+2
View File
@@ -16,5 +16,7 @@ import { defineConfig } from "vite";
export default defineConfig({
base: process.env.BASE_PATH ?? "/",
plugins: [react(), tailwindcss()],
// Fixed PenguinHarness dev port (stands alone — only the main server default is
// shared, as DEFAULT_SERVER_PORT in core; vite configs cannot import core TS).
server: { port: 7367 },
});
+2
View File
@@ -13,5 +13,7 @@ import { defineConfig } from "vite";
export default defineConfig({
base: process.env.BASE_PATH ?? "/",
plugins: [react(), tailwindcss()],
// Fixed PenguinHarness dev port (stands alone — only the main server default is
// shared, as DEFAULT_SERVER_PORT in core; vite configs cannot import core TS).
server: { port: 7366 },
});
+2 -2
View File
@@ -12,7 +12,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRoot } from "@prismshadow/penguin-core";
import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
export interface ServerConfig {
/** Local data root directory (shared with the SDK/CLI). */
@@ -52,7 +52,7 @@ export function resolveServerConfig(env: NodeJS.ProcessEnv = process.env): Serve
// An empty PORT string is treated as unset (the common `.env` case of an empty
// `PORT=`): Number("") === 0 would pass the range check and bind to a random
// port; this matches the CLI's resolvePort convention.
const port = Number(env.PORT || 7364);
const port = Number(env.PORT || DEFAULT_SERVER_PORT);
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`Invalid port configuration PORT=${env.PORT}`);
}
+2
View File
@@ -14,6 +14,8 @@ import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
// Fixed PenguinHarness dev port (stands alone — only the main server default is
// shared, as DEFAULT_SERVER_PORT in core; vite configs cannot import core TS).
port: 7365,
proxy: {
"/api": {