Stop publishing repository-wide prompt ledgers and enforce per-user prompt isolation through authenticated Gitea IDs.

Hop-State: A_06FN8NFDRFAFN5VCJTHJAAG
Hop-Proposal: R_06FN8NEGJ054CPMJF9ZAN70
Hop-Task: T_06FN8HT6XG332C3XY6PYCCG
Hop-Attempt: AT_06FN8HT6XGRXW0B4WPFFBJG
This commit is contained in:
2026-07-11 20:19:05 -07:00
committed by Hop
parent 56755a77fd
commit 12c806c77f
104 changed files with 255 additions and 1626 deletions
+3
View File
@@ -44,6 +44,9 @@ docker compose --env-file "$env_file" cp \
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native-v16.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/js/hop-native.js \
gitea:/data/gitea/public/assets/js/hop-native-v17.js
docker compose --env-file "$env_file" cp \
deploy/gitea/public/assets/css/hop-home.css \
gitea:/data/gitea/public/assets/css/hop-home.css
+21 -52
View File
@@ -184,45 +184,23 @@
return details;
}
async function portablePromptRecords(path) {
async function personalPromptRecords(path) {
const [owner, repo] = path.slice(1).split('/');
let branches = ['main', 'master'];
try {
const repository = await fetch(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {credentials: 'same-origin'});
if (repository.ok) {
const payload = await repository.json();
if (payload.default_branch) branches = [payload.default_branch, ...branches.filter((branch) => branch !== payload.default_branch)];
}
} catch (_) {
// The raw-file fallback below still works for the common default branches.
}
for (const branch of branches) {
const treeResponse = await fetch(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(branch)}?recursive=true`, {credentials: 'same-origin'});
if (treeResponse.status === 404) continue;
if (!treeResponse.ok) throw new Error('Could not inspect the published prompt records');
const tree = await treeResponse.json();
const records = (Array.isArray(tree.tree) ? tree.tree : [])
.filter((entry) => entry.type === 'blob' && /^\.hop\/records\/prompts\/[^/]+\.json$/.test(entry.path));
if (!records.length) return [];
const prompts = await Promise.all(records.map(async (entry) => {
const recordPath = entry.path.split('/').map(encodeURIComponent).join('/');
const response = await fetch(`${path}/raw/branch/${encodeURIComponent(branch)}/${recordPath}`, {credentials: 'same-origin'});
if (!response.ok) throw new Error('Could not load a published prompt record');
return response.json();
}));
return prompts
.filter((prompt) => {
const request = String(prompt.prompt || '');
const outcome = String(prompt.response_summary || '');
return prompt.status !== 'active'
&& prompt.status !== 'running'
&& !request.startsWith('Resolve proposal ')
&& !outcome.startsWith('hop-reconciliation-conflicts:');
})
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)));
}
return [];
const query = new URLSearchParams({owner, repo});
const response = await fetch(`/hop/api/v1/prompts?${query}`, {credentials: 'same-origin'});
if (response.status === 401) throw new Error('Sign in to view your prompt history.');
if (!response.ok) throw new Error('Could not load your prompt history.');
const payload = await response.json();
return (Array.isArray(payload.prompts) ? payload.prompts : [])
.filter((prompt) => {
const request = String(prompt.prompt || '');
const outcome = String(prompt.response_summary || '');
return prompt.status !== 'active'
&& prompt.status !== 'running'
&& !request.startsWith('Resolve proposal ')
&& !outcome.startsWith('hop-reconciliation-conflicts:');
})
.sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)));
}
function renderPromptPage() {
@@ -244,7 +222,7 @@
heading.append(
makeElement('p', 'hop-prompts__eyebrow', 'Hop causal record'),
makeElement('h1', '', 'Prompts'),
makeElement('p', 'hop-prompts__lede', 'Review what you requested, which agent acted, and the proposal that resulted. Internal agent instructions are not published.'),
makeElement('p', 'hop-prompts__lede', 'Review your requests for this repository. Prompt history is private to your signed-in account.'),
);
heading.querySelector('h1').id = 'hop-prompts-title';
const refresh = makeElement('button', 'ui button hop-prompts__refresh', 'Refresh');
@@ -260,27 +238,18 @@
status.textContent = 'Loading prompt records…';
page.querySelector('.hop-prompt-list, .hop-prompts__empty')?.remove();
try {
const prompts = await portablePromptRecords(path);
const prompts = await personalPromptRecords(path);
if (!prompts.length) {
status.textContent = 'No published prompt records';
status.textContent = 'No prompts for your account';
const empty = makeElement('div', 'hop-prompts__empty');
empty.append(
makeElement('strong', '', 'No prompts have been published yet.'),
document.createTextNode('Hop publishes an immutable record for each prompt when it creates a proposal. Those files travel with the repository and appear here after the push.'),
makeElement('strong', '', 'You have no prompt history for this repository.'),
document.createTextNode('Only requests attributed to your signed-in Gitea account appear here.'),
);
page.append(empty);
return;
}
status.textContent = prompts.length === 1 ? '1 captured request' : `${prompts.length} captured requests`;
if (!prompts.length) {
const empty = makeElement('div', 'hop-prompts__empty');
empty.append(
makeElement('strong', '', 'The published prompt record set is empty.'),
document.createTextNode('Capture work through Hop and create a proposal to publish its prompt records.'),
);
page.append(empty);
return;
}
const list = makeElement('div', 'hop-prompt-list');
prompts.forEach((prompt) => list.append(promptRow(prompt)));
page.append(list);
+1 -1
View File
@@ -1,3 +1,3 @@
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/hop-home.css?v=4">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/hop-prompts.css?v=4">
<script src="{{AssetUrlPrefix}}/js/hop-native-v16.js?v=1"></script>
<script src="{{AssetUrlPrefix}}/js/hop-native-v17.js?v=1"></script>