@promptbook/cli 0.112.0-121 → 0.112.0-124
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 +14 -14
- package/agents/default/developer.book +23 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/messages/route.ts +4 -11
- package/apps/agents-server/src/app/api/scrape/route.ts +18 -0
- package/apps/agents-server/src/tools/createAgentProgressTools.ts +6 -4
- package/apps/agents-server/src/utils/assertSafeUrl.ts +136 -0
- package/apps/agents-server/src/utils/authenticateUser.ts +2 -1
- package/apps/agents-server/src/utils/getCurrentUser.ts +2 -1
- package/apps/agents-server/src/utils/isAdminPasswordEqual.ts +28 -0
- package/apps/agents-server/src/utils/isUserGlobalAdmin.ts +3 -1
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +21 -0
- package/apps/agents-server/src/utils/userChat/retryUserChatJob.ts +4 -0
- package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +2 -0
- package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +288 -0
- package/esm/index.es.js +1430 -283
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +6 -0
- package/esm/scripts/run-codex-prompts/main/findUnwrittenPrompts.d.ts +19 -0
- package/esm/scripts/run-codex-prompts/main/runCodexPromptsServer.d.ts +27 -0
- package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/esm/scripts/run-codex-prompts/server/coderServerHtml.d.ts +9 -0
- package/esm/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +23 -0
- package/esm/scripts/run-codex-prompts/server/updatePromptSection.d.ts +9 -0
- package/esm/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
- package/esm/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
- package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
- package/esm/src/cli/cli-commands/coder/server.d.ts +13 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.ts +79 -0
- package/src/cli/cli-commands/coder/find-unwritten.ts +58 -0
- package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
- package/src/cli/cli-commands/coder/init.ts +6 -1
- package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +10 -0
- package/src/cli/cli-commands/coder/printInitializationSummary.ts +3 -0
- package/src/cli/cli-commands/coder/run.ts +26 -11
- package/src/cli/cli-commands/coder/server.ts +239 -0
- package/src/cli/cli-commands/coder.ts +6 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +727 -1105
- package/src/version.ts +2 -2
- package/src/versions.txt +3 -1
- package/umd/index.umd.js +1431 -284
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/resolveAgentSystemMessage.d.ts +6 -0
- package/umd/scripts/run-codex-prompts/main/findUnwrittenPrompts.d.ts +19 -0
- package/umd/scripts/run-codex-prompts/main/runCodexPromptsServer.d.ts +27 -0
- package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
- package/umd/scripts/run-codex-prompts/server/coderServerHtml.d.ts +9 -0
- package/umd/scripts/run-codex-prompts/server/runCoderHttpServer.d.ts +23 -0
- package/umd/scripts/run-codex-prompts/server/updatePromptSection.d.ts +9 -0
- package/umd/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
- package/umd/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
- package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
- package/umd/src/cli/cli-commands/coder/server.d.ts +13 -0
- package/umd/src/version.d.ts +1 -1
package/README.md
CHANGED
|
@@ -541,7 +541,7 @@ Prompts marked with `[-]` are not ready yet, prompts containing `@@@` are treate
|
|
|
541
541
|
#### Features
|
|
542
542
|
|
|
543
543
|
- **Multi-runner execution:** `openai-codex`, `github-copilot`, `cline`, `claude-code`, `opencode`, `gemini`
|
|
544
|
-
- **Context injection:** `--context AGENTS.md` or inline extra instructions
|
|
544
|
+
- **Context injection:** `--agent agents/coding/developer.book --context AGENTS.md` or inline extra instructions
|
|
545
545
|
- **Reasoning control:** `--thinking-level low|medium|high|xhigh` for supported runners
|
|
546
546
|
- **Interactive or unattended runs:** default wait mode, or `--no-wait` for batch execution
|
|
547
547
|
- **Git safety:** clean working tree check by default, optional `--ignore-git-changes`
|
|
@@ -561,11 +561,11 @@ npx ts-node ./src/cli/test/ptbk.ts coder generate-boilerplates --template prompt
|
|
|
561
561
|
|
|
562
562
|
npx ts-node ./src/cli/test/ptbk.ts coder generate-boilerplates --template prompts/templates/agents-server.md
|
|
563
563
|
|
|
564
|
-
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md
|
|
564
|
+
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md
|
|
565
565
|
|
|
566
|
-
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
|
|
566
|
+
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --auto-push
|
|
567
567
|
|
|
568
|
-
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --ignore-git-changes --no-wait
|
|
568
|
+
npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --ignore-git-changes --no-wait
|
|
569
569
|
|
|
570
570
|
npx ts-node ./src/cli/test/ptbk.ts coder find-refactor-candidates
|
|
571
571
|
|
|
@@ -587,11 +587,11 @@ ptbk coder generate-boilerplates
|
|
|
587
587
|
|
|
588
588
|
ptbk coder generate-boilerplates --template prompts/templates/common.md
|
|
589
589
|
|
|
590
|
-
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test
|
|
590
|
+
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --test npm run test
|
|
591
591
|
|
|
592
|
-
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
|
|
592
|
+
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --auto-push
|
|
593
593
|
|
|
594
|
-
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
|
|
594
|
+
ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
|
|
595
595
|
|
|
596
596
|
ptbk coder find-refactor-candidates
|
|
597
597
|
|
|
@@ -604,13 +604,13 @@ ptbk coder verify
|
|
|
604
604
|
|
|
605
605
|
#### What each command does
|
|
606
606
|
|
|
607
|
-
| Command | What it does
|
|
608
|
-
| ------------------------------------- |
|
|
607
|
+
| Command | What it does |
|
|
608
|
+
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ---- | ----- | ------------------------------------------------------------------------ |
|
|
609
609
|
| `ptbk coder init` | Creates `prompts/`, `prompts/done/`, the project-generic template files materialized in `prompts/templates/` (currently `common.md`), and a starter `AGENTS.md`; ensures `.env` contains `CODING_AGENT_GIT_NAME`, `CODING_AGENT_GIT_EMAIL`, and `CODING_AGENT_GIT_SIGNING_KEY`; adds helper coder scripts to `package.json`; ensures `.gitignore` contains `/.promptbook`; and configures `.vscode/settings.json` to save pasted prompt images into `prompts/screenshots/`. |
|
|
610
|
-
| `ptbk coder generate-boilerplates` | Creates new prompt markdown files with fresh emoji tags so you can quickly fill in coding tasks; `--template` accepts either a built-in alias or a markdown file path relative to the project root.
|
|
611
|
-
| `ptbk coder run` | Picks the next ready prompt, appends optional context, runs it through the selected coding agent, can optionally verify each attempt with a shell test command and feed failing output back for retries, then marks success or failure, commits the result, and pushes only when `--auto-push` is enabled.
|
|
612
|
-
| `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <xlow
|
|
613
|
-
| `ptbk coder verify` | Walks through completed prompts, archives truly finished work, and adds follow-up repair prompts for unfinished results.
|
|
610
|
+
| `ptbk coder generate-boilerplates` | Creates new prompt markdown files with fresh emoji tags so you can quickly fill in coding tasks; `--template` accepts either a built-in alias or a markdown file path relative to the project root. |
|
|
611
|
+
| `ptbk coder run` | Picks the next ready prompt, appends optional context, runs it through the selected coding agent, can optionally verify each attempt with a shell test command and feed failing output back for retries, then marks success or failure, commits the result, and pushes only when `--auto-push` is enabled. |
|
|
612
|
+
| `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <xlow | low | medium | high | xhigh | extreme>` ranges from a very benevolent scan to a very aggressive sweep. |
|
|
613
|
+
| `ptbk coder verify` | Walks through completed prompts, archives truly finished work, and adds follow-up repair prompts for unfinished results. |
|
|
614
614
|
|
|
615
615
|
#### Most useful `ptbk coder run` flags
|
|
616
616
|
|
|
@@ -633,7 +633,7 @@ ptbk coder verify
|
|
|
633
633
|
|
|
634
634
|
1. Initialize once with `ptbk coder init`.
|
|
635
635
|
2. Customize `prompts/templates/*.md` if needed, then create or write prompt files in `prompts/`.
|
|
636
|
-
3. Customize the starter `AGENTS.md` with repository-specific instructions, then pass `--context AGENTS.md`.
|
|
636
|
+
3. Customize the starter `AGENTS.md` with repository-specific instructions, then pass `--agent agents/coding/developer.book --context AGENTS.md`.
|
|
637
637
|
4. Run one prompt at a time interactively, or use `--no-wait` for unattended batches.
|
|
638
638
|
5. Finish with `ptbk coder verify` so resolved prompts are archived and broken ones get explicit repair follow-ups.
|
|
639
639
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Promptbook Developer
|
|
2
|
+
|
|
3
|
+
RULE
|
|
4
|
+
Keep in mind the DRY _(don't repeat yourself)_ principle.
|
|
5
|
+
|
|
6
|
+
RULE
|
|
7
|
+
Keep in mind the SOLID principles.
|
|
8
|
+
|
|
9
|
+
RULE
|
|
10
|
+
Do a proper analysis of the current functionality before you start implementing.
|
|
11
|
+
|
|
12
|
+
RULE
|
|
13
|
+
Keep small responsibilities of functions and classes, avoid creating big functions or classes that do many things.
|
|
14
|
+
|
|
15
|
+
RULE
|
|
16
|
+
Constants should always be `UPPER_SNAKE_CASE`.
|
|
17
|
+
|
|
18
|
+
RULE
|
|
19
|
+
Boolean variables should always be prefixed with `is`, for example `isUserChatJobLeaseExpired` or `IS_DEBUG_MODE`.
|
|
20
|
+
|
|
21
|
+
RULE
|
|
22
|
+
Do not use abbreviations, for example use `isExpired` instead of `isExp`, `translateMessage` instead of `t`, etc.
|
|
23
|
+
It is fine to use well-known abbreviations, for example `id`, `url`, `html`, etc.
|
package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/messages/route.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
getUserChatJobByClientMessageId,
|
|
13
13
|
isFrozenUserChatSource,
|
|
14
14
|
resolveUserChatReplyReference,
|
|
15
|
-
runImmediateUserChatAnswer,
|
|
16
15
|
triggerUserChatJobWorker,
|
|
17
16
|
} from '@/src/utils/userChat';
|
|
18
17
|
import { UserChatReplyValidationError } from '@/src/utils/userChat/UserChatReplyValidationError';
|
|
@@ -171,16 +170,10 @@ export async function POST(
|
|
|
171
170
|
|
|
172
171
|
after(() => {
|
|
173
172
|
const origin = new URL(request.url).origin;
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}).catch((error) => console.error('[user-chat] Failed to trigger durable worker', error)),
|
|
179
|
-
runImmediateUserChatAnswer(enqueuedTurn.job, {
|
|
180
|
-
agentSource: resolvedAgentContext.resolvedAgentSource,
|
|
181
|
-
resolvedAgentName: resolvedAgentContext.resolvedAgentName,
|
|
182
|
-
}).catch((error) => console.error('[user-chat] Failed to start immediate chat answer', error)),
|
|
183
|
-
]).then(() => undefined);
|
|
173
|
+
return triggerUserChatJobWorker({
|
|
174
|
+
origin,
|
|
175
|
+
preferredJobId: enqueuedTurn.job.id,
|
|
176
|
+
}).catch((error) => console.error('[user-chat] Failed to trigger durable worker', error));
|
|
184
177
|
});
|
|
185
178
|
|
|
186
179
|
return NextResponse.json(
|
|
@@ -2,6 +2,8 @@ import { serializeError } from '@promptbook-local/utils';
|
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { fetchUrlContent } from '../../../../../../src/commitments/USE_BROWSER/fetchUrlContent';
|
|
4
4
|
import { assertsError } from '../../../../../../src/errors/assertsError';
|
|
5
|
+
import { getCurrentUser } from '../../../utils/getCurrentUser';
|
|
6
|
+
import { assertSafeUrl } from '../../../utils/assertSafeUrl';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* API endpoint for scraping URL content
|
|
@@ -9,9 +11,17 @@ import { assertsError } from '../../../../../../src/errors/assertsError';
|
|
|
9
11
|
* This endpoint proxies the server-side scraping functionality to be accessible from the browser.
|
|
10
12
|
* It uses the fetchUrlContent utility to fetch and convert web content to markdown.
|
|
11
13
|
*
|
|
14
|
+
* Requires authentication to prevent unauthenticated SSRF abuse.
|
|
15
|
+
* The destination URL is validated against private/internal IP ranges before fetching.
|
|
16
|
+
*
|
|
12
17
|
* @route GET /api/scrape?url=<url>
|
|
13
18
|
*/
|
|
14
19
|
export async function GET(request: NextRequest) {
|
|
20
|
+
const currentUser = await getCurrentUser();
|
|
21
|
+
if (!currentUser) {
|
|
22
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
try {
|
|
16
26
|
// Get the URL parameter from the query string
|
|
17
27
|
const searchParams = request.nextUrl.searchParams;
|
|
@@ -22,6 +32,14 @@ export async function GET(request: NextRequest) {
|
|
|
22
32
|
return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
// Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
|
|
36
|
+
try {
|
|
37
|
+
assertSafeUrl(url);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
assertsError(error);
|
|
40
|
+
return NextResponse.json({ error: error.message, success: false }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
// Use the server-side fetchUrlContent utility
|
|
26
44
|
const content = await fetchUrlContent(url);
|
|
27
45
|
|
|
@@ -22,11 +22,13 @@ export function createAgentProgressTools(
|
|
|
22
22
|
{
|
|
23
23
|
name: AGENT_PROGRESS_TOOL_NAME,
|
|
24
24
|
description: spaceTrim(`
|
|
25
|
-
|
|
25
|
+
Publish real-time progress to the user while you work on a response.
|
|
26
26
|
|
|
27
|
-
- Use this
|
|
28
|
-
-
|
|
29
|
-
-
|
|
27
|
+
- Use this for concrete progress, not placeholder reassurance.
|
|
28
|
+
- Call it before any chunk of work that may take more than a few seconds.
|
|
29
|
+
- Update it when you start a step, call or finish a tool, learn a result, or decide the next step.
|
|
30
|
+
- Include what you are doing now, visible actions/results, and what you will do next.
|
|
31
|
+
- Keep updates concise and user-facing; do not expose hidden chain-of-thought.
|
|
30
32
|
`),
|
|
31
33
|
parameters: {
|
|
32
34
|
type: 'object',
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from 'net';
|
|
2
|
+
import { spaceTrim } from 'spacetrim';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Private IPv4 CIDR ranges that must never be reached via the scrape proxy.
|
|
6
|
+
* Covers loopback, RFC 1918 private ranges, link-local (APIPA / cloud metadata),
|
|
7
|
+
* and the shared address space (RFC 6598).
|
|
8
|
+
*/
|
|
9
|
+
const PRIVATE_IPV4_RANGES: ReadonlyArray<{ readonly network: string; readonly prefixBits: number }> = [
|
|
10
|
+
{ network: '127.0.0.0', prefixBits: 8 }, // loopback
|
|
11
|
+
{ network: '10.0.0.0', prefixBits: 8 }, // RFC 1918
|
|
12
|
+
{ network: '172.16.0.0', prefixBits: 12 }, // RFC 1918
|
|
13
|
+
{ network: '192.168.0.0', prefixBits: 16 }, // RFC 1918
|
|
14
|
+
{ network: '169.254.0.0', prefixBits: 16 }, // link-local / AWS metadata (169.254.169.254)
|
|
15
|
+
{ network: '0.0.0.0', prefixBits: 8 }, // "this" network
|
|
16
|
+
{ network: '100.64.0.0', prefixBits: 10 }, // shared address space (RFC 6598)
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts an IPv4 address string to an unsigned 32-bit integer.
|
|
21
|
+
*/
|
|
22
|
+
function ipv4ToUint32(ip: string): number {
|
|
23
|
+
return ip.split('.').reduce((accumulator, octet) => (accumulator << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true when the given IPv4 address falls within any private/reserved range.
|
|
28
|
+
*/
|
|
29
|
+
function isPrivateIPv4(ip: string): boolean {
|
|
30
|
+
const ipAsNumber = ipv4ToUint32(ip);
|
|
31
|
+
for (const { network, prefixBits } of PRIVATE_IPV4_RANGES) {
|
|
32
|
+
const networkMask = prefixBits === 0 ? 0 : (~((1 << (32 - prefixBits)) - 1)) >>> 0;
|
|
33
|
+
if ((ipAsNumber & networkMask) === (ipv4ToUint32(network) & networkMask)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true when the given IPv6 address falls within any private/reserved range.
|
|
42
|
+
* Covers loopback (::1), unspecified (::), IPv4-mapped addresses (::ffff:A.B.C.D),
|
|
43
|
+
* ULA (fc00::/7), and link-local (fe80::/10).
|
|
44
|
+
*/
|
|
45
|
+
function isPrivateIPv6(ip: string): boolean {
|
|
46
|
+
const normalized = ip.toLowerCase();
|
|
47
|
+
|
|
48
|
+
if (normalized === '::1' || normalized === '::') {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// IPv4-mapped: ::ffff:A.B.C.D — delegate to the IPv4 check
|
|
53
|
+
if (normalized.startsWith('::ffff:')) {
|
|
54
|
+
const ipv4Part = normalized.slice('::ffff:'.length);
|
|
55
|
+
if (isIPv4(ipv4Part)) {
|
|
56
|
+
return isPrivateIPv4(ipv4Part);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const firstGroup = parseInt((normalized.split(':')[0] ?? '0') || '0', 16);
|
|
61
|
+
|
|
62
|
+
// ULA: fc00::/7 — first 7 bits are 1111110
|
|
63
|
+
if ((firstGroup & 0xfe00) === 0xfc00) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Link-local: fe80::/10 — first 10 bits are 1111111010
|
|
68
|
+
if ((firstGroup & 0xffc0) === 0xfe80) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Asserts that a URL is safe to use as a scrape target, blocking SSRF attacks.
|
|
77
|
+
*
|
|
78
|
+
* Throws an error when:
|
|
79
|
+
* - The URL scheme is not `http:` or `https:`
|
|
80
|
+
* - The hostname is `localhost`
|
|
81
|
+
* - The hostname is a private/reserved IPv4 address (RFC 1918, loopback, link-local, …)
|
|
82
|
+
* - The hostname is a private/reserved IPv6 address (ULA, link-local, loopback, …)
|
|
83
|
+
*
|
|
84
|
+
* @throws {Error} when the URL would allow reaching an internal/private resource
|
|
85
|
+
*/
|
|
86
|
+
export function assertSafeUrl(url: string): void {
|
|
87
|
+
let parsedUrl: URL;
|
|
88
|
+
try {
|
|
89
|
+
parsedUrl = new URL(url);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`Invalid URL: \`${url}\``);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
95
|
+
throw new Error(spaceTrim(`
|
|
96
|
+
URL scheme \`${parsedUrl.protocol}\` is not allowed.
|
|
97
|
+
|
|
98
|
+
Only \`http:\` and \`https:\` schemes are permitted.
|
|
99
|
+
`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
103
|
+
|
|
104
|
+
if (hostname === 'localhost') {
|
|
105
|
+
throw new Error(
|
|
106
|
+
spaceTrim(`
|
|
107
|
+
Requests to \`localhost\` are not allowed.
|
|
108
|
+
|
|
109
|
+
Only public, externally reachable URLs may be scraped.
|
|
110
|
+
`),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isIPv4(hostname) && isPrivateIPv4(hostname)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
spaceTrim(`
|
|
117
|
+
Requests to private IPv4 address \`${hostname}\` are not allowed.
|
|
118
|
+
|
|
119
|
+
Only public, externally reachable URLs may be scraped.
|
|
120
|
+
`),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// IPv6 addresses appear inside square brackets in URLs: http://[::1]/path
|
|
125
|
+
const ipv6Candidate = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
126
|
+
|
|
127
|
+
if (isIPv6(ipv6Candidate) && isPrivateIPv6(ipv6Candidate)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
spaceTrim(`
|
|
130
|
+
Requests to private IPv6 address \`${ipv6Candidate}\` are not allowed.
|
|
131
|
+
|
|
132
|
+
Only public, externally reachable URLs may be scraped.
|
|
133
|
+
`),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -2,6 +2,7 @@ import { $getTableName } from '../database/$getTableName';
|
|
|
2
2
|
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
3
3
|
import { AgentsServerDatabase } from '../database/schema';
|
|
4
4
|
import { verifyPassword } from './auth';
|
|
5
|
+
import { isAdminPasswordEqual } from './isAdminPasswordEqual';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Type describing authenticated user.
|
|
@@ -17,7 +18,7 @@ export type AuthenticatedUser = {
|
|
|
17
18
|
*/
|
|
18
19
|
export async function authenticateUser(username: string, password: string): Promise<AuthenticatedUser | null> {
|
|
19
20
|
// 1. Check if it's the environment admin
|
|
20
|
-
if (username === 'admin' &&
|
|
21
|
+
if (username === 'admin' && isAdminPasswordEqual(password)) {
|
|
21
22
|
return { username: 'admin', isAdmin: true, isGlobalAdmin: true };
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -3,6 +3,7 @@ import { cache } from 'react';
|
|
|
3
3
|
import { $getTableName } from '../database/$getTableName';
|
|
4
4
|
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
5
5
|
import type { AgentsServerDatabase } from '../database/schema';
|
|
6
|
+
import { isAdminPasswordEqual } from './isAdminPasswordEqual';
|
|
6
7
|
import { getSession } from './session';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -75,7 +76,7 @@ const getCachedCurrentUser = cache(async (): Promise<UserInfo | null> => {
|
|
|
75
76
|
if (process.env.ADMIN_PASSWORD) {
|
|
76
77
|
const cookieStore = await cookies();
|
|
77
78
|
const adminToken = cookieStore.get('adminToken');
|
|
78
|
-
if (adminToken?.value
|
|
79
|
+
if (adminToken?.value !== undefined && isAdminPasswordEqual(adminToken.value)) {
|
|
79
80
|
return {
|
|
80
81
|
username: 'admin',
|
|
81
82
|
isAdmin: true,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compares a candidate string against the `ADMIN_PASSWORD` environment variable
|
|
5
|
+
* using a constant-time algorithm to prevent timing attacks.
|
|
6
|
+
*
|
|
7
|
+
* Returning `false` early when the byte lengths differ does not expose character
|
|
8
|
+
* content — only length — which is an acceptable trade-off, since `timingSafeEqual`
|
|
9
|
+
* requires equal-length buffers and length is typically not secret.
|
|
10
|
+
*
|
|
11
|
+
* @param candidate The value to compare against the configured admin password.
|
|
12
|
+
* @returns `true` when `candidate` matches `ADMIN_PASSWORD`, `false` otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export function isAdminPasswordEqual(candidate: string): boolean {
|
|
15
|
+
const adminPassword = process.env.ADMIN_PASSWORD;
|
|
16
|
+
if (!adminPassword) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const candidateBuffer = Buffer.from(candidate);
|
|
21
|
+
const adminPasswordBuffer = Buffer.from(adminPassword);
|
|
22
|
+
|
|
23
|
+
if (candidateBuffer.length !== adminPasswordBuffer.length) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return timingSafeEqual(candidateBuffer, adminPasswordBuffer);
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cookies } from 'next/headers';
|
|
2
2
|
import { cache } from 'react';
|
|
3
|
+
import { isAdminPasswordEqual } from './isAdminPasswordEqual';
|
|
3
4
|
import { getSession } from './session';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -18,7 +19,8 @@ const getCachedIsUserGlobalAdmin = cache(async (): Promise<boolean> => {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const cookieStore = await cookies();
|
|
21
|
-
|
|
22
|
+
const adminToken = cookieStore.get('adminToken')?.value;
|
|
23
|
+
return adminToken !== undefined && isAdminPasswordEqual(adminToken);
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -4,6 +4,7 @@ import type { ChatPromptResult } from '../../../../../src/execution/PromptResult
|
|
|
4
4
|
import { mergeToolCalls } from '../../../../../src/utils/toolCalls/mergeToolCalls';
|
|
5
5
|
import { persistUserChatJobTerminalState } from './persistUserChatJobTerminalState';
|
|
6
6
|
import { updateUserChatAssistantMessage } from './updateUserChatAssistantMessage';
|
|
7
|
+
import { createRunningUserChatProgressCard } from './userChatProgressCard';
|
|
7
8
|
import { USER_CHAT_JOB_ASSISTANT_MESSAGE_PERSIST_INTERVAL_MS } from './userChatJobRuntimeConstants';
|
|
8
9
|
import type { UserChatJobRecord } from './UserChatJobRecord';
|
|
9
10
|
|
|
@@ -44,6 +45,14 @@ export function createRunUserChatJobPersistenceController(options: {
|
|
|
44
45
|
let hasPendingAssistantMessageUpdate = false;
|
|
45
46
|
let assistantMessageUpdateTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Creates a typed ISO timestamp for progress-card updates.
|
|
50
|
+
*
|
|
51
|
+
* @private function of `createRunUserChatJobPersistenceController`
|
|
52
|
+
*/
|
|
53
|
+
const createProgressUpdatedAt = (): NonNullable<ChatMessage['progressCard']>['updatedAt'] =>
|
|
54
|
+
new Date().toISOString() as NonNullable<ChatMessage['progressCard']>['updatedAt'];
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
* Queues one assistant-message mutation behind any earlier persistence work.
|
|
49
58
|
*
|
|
@@ -132,6 +141,12 @@ export function createRunUserChatJobPersistenceController(options: {
|
|
|
132
141
|
lifecycleState: 'running',
|
|
133
142
|
lifecycleError: undefined,
|
|
134
143
|
isComplete: false,
|
|
144
|
+
progressCard: createRunningUserChatProgressCard({
|
|
145
|
+
currentProgressCard: message.progressCard,
|
|
146
|
+
content: latestContent,
|
|
147
|
+
toolCalls: latestToolCalls,
|
|
148
|
+
updatedAt: createProgressUpdatedAt(),
|
|
149
|
+
}),
|
|
135
150
|
prompt: options.createPromptSnapshot({
|
|
136
151
|
toolCalls: latestToolCalls,
|
|
137
152
|
}),
|
|
@@ -186,6 +201,12 @@ export function createRunUserChatJobPersistenceController(options: {
|
|
|
186
201
|
lifecycleState: 'running',
|
|
187
202
|
lifecycleError: undefined,
|
|
188
203
|
isComplete: false,
|
|
204
|
+
progressCard: createRunningUserChatProgressCard({
|
|
205
|
+
currentProgressCard: message.progressCard,
|
|
206
|
+
content: latestContent,
|
|
207
|
+
toolCalls: latestToolCalls,
|
|
208
|
+
updatedAt: createProgressUpdatedAt(),
|
|
209
|
+
}),
|
|
189
210
|
availableTools: options.availableTools,
|
|
190
211
|
prompt: options.createPromptSnapshot(),
|
|
191
212
|
}),
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { UserChatJobRecord } from './UserChatJobRecord';
|
|
2
2
|
import type { UserChatJobRow } from './UserChatJobRow';
|
|
3
3
|
import type { Json } from '@/src/database/schema';
|
|
4
|
+
import type { ChatMessage } from '@promptbook-local/types';
|
|
4
5
|
import { withoutLocalUserChatJobMetadata } from '../localChatRunner/LocalUserChatJobMetadata';
|
|
5
6
|
import { getUserChatJobById } from './getUserChatJobById';
|
|
6
7
|
import { mapUserChatJobRow } from './mapUserChatJobRow';
|
|
7
8
|
import { provideUserChatJobTable } from './provideUserChatJobTable';
|
|
8
9
|
import { updateUserChatAssistantMessage } from './updateUserChatAssistantMessage';
|
|
10
|
+
import { createQueuedUserChatProgressCard } from './userChatProgressCard';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Requeues one failed durable chat job and resets its assistant placeholder.
|
|
@@ -23,6 +25,7 @@ export async function retryUserChatJob(jobId: string): Promise<UserChatJobRecord
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const nowIso = new Date().toISOString();
|
|
28
|
+
const progressUpdatedAt = nowIso as NonNullable<ChatMessage['progressCard']>['updatedAt'];
|
|
26
29
|
const userChatJobTable = await provideUserChatJobTable();
|
|
27
30
|
const { data, error } = await userChatJobTable
|
|
28
31
|
.update({
|
|
@@ -67,6 +70,7 @@ export async function retryUserChatJob(jobId: string): Promise<UserChatJobRecord
|
|
|
67
70
|
completedToolCalls: undefined,
|
|
68
71
|
prompt: undefined,
|
|
69
72
|
generationDurationMs: undefined,
|
|
73
|
+
progressCard: createQueuedUserChatProgressCard(progressUpdatedAt),
|
|
70
74
|
}),
|
|
71
75
|
});
|
|
72
76
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage } from '../../../../../src/_packages/types.index';
|
|
2
|
+
import { createQueuedUserChatProgressCard } from './userChatProgressCard';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Lifecycle states rendered on canonical server-owned chat messages.
|
|
@@ -45,6 +46,7 @@ export function createQueuedUserChatAssistantMessage(options: {
|
|
|
45
46
|
isComplete: false,
|
|
46
47
|
lifecycleState: 'queued',
|
|
47
48
|
jobId: options.jobId,
|
|
49
|
+
progressCard: createQueuedUserChatProgressCard(options.createdAt),
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|