Skip to content

feat(chat): let Project Assistant read Template README - #339

Merged
zjy365 merged 9 commits into
mainfrom
codex/project-context-153
Sep 10, 2026
Merged

zjy365 merged 9 commits into
mainfrom
codex/project-context-153

Conversation

@zjy365

@zjy365 zjy365 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Let the Project Assistant read the current Project's Template README when answering application usage and configuration questions. Add one readTemplateReadme tool that returns documentation through the existing Chat tool-result flow.

  • Resolve Template names from Project-scoped Deployment Task sources and adopted Template Instance records. Read immediately for one Template; return names for selection when several are recorded.
  • Reuse the existing Template Provider getTemplateSource?includeReadme=true endpoint and its README retrieval. Return only README text, with a 15-second request budget, a 2 MiB response cap, and explicit truncation at 32,000 characters.
  • Propagate user cancellation. Return a retryable result for the 15-second retrieval timeout and an explicit error when the entire provider JSON (including YAML and README) exceeds 2 MiB. Return ordinary tool errors for missing documentation, unknown Templates, or provider failures so other Chat tools remain usable. Treat README as external documentation, not live state or instructions that override the Assistant.
  • Limit the tool to Project conversations and bind Project/namespace on the server. Update ADR 0077 to describe the reader.

This PR adds no database migration, environment variable, public route, dependency, or approval requirement. It does not add a general Project Context Index or Runtime Contract, and does not change deployment routing or the direct-execution behavior on main.

Refs labring/sealos-private#154. The broader index proposed in labring/sealos-private#153 is not delivered by this PR.

  • Organize system instructions around Sealos product capabilities and environment, user collaboration, and essential operation rules. Explain Projects, AP/DB resources, asynchronous deployments, and UI surfaces; distinguish discussion from execution and opening a surface from reading or changing a resource. Tool selection follows the user goal, available context, and information source. Workspace quota snapshots remain attached, with usage/limit and workspace scope explained. Devbox is a separate execution sandbox. Preserve deployment routing and direct execution rules.
  • Group Chat Langfuse traces by the verified workspace namespace (ns-...) in userId; conversation ownership continues to use the global user UID.

Validation

  • Rebased onto main at ffce30ee (includes feat(chat): add guarded Pi-style Devbox tools #320 and fix(chat): execute tools directly by default and unblock pending approvals #350).

  • 24 focused tests passed after review fixes (including cancellation, body-read timeout, and short README with oversized YAML): README selection, SQL Project/namespace isolation and adopted Templates, tool-result conversion to model context, provider failures/cancellation/limits, and existing provider behavior.

  • 69 Chat route tests passed in a separate process.

  • bun typecheck, bun check, bun lint, and git diff --check passed.

  • Live check through the new provider reader returned the Memos README: 1,263 characters, not truncated.

  • Final product-capability prompt revision: 8 context/quota tests and 69 Chat route tests passed; bun typecheck and bun check passed. Live session confirmed README retrieval before this revision; the revised tool-selection wording still needs a model conversation replay.

Limits

The Template Provider must support and enable README retrieval. Projects without a recorded Template source cannot resolve a README. Returned documentation is the provider's current version; historical deployment-version matching is not added. The live check covered the provider reader, not a complete browser conversation with a model.

@zjy365
zjy365 force-pushed the codex/project-context-153 branch from 319a14a to b1b9b30 Compare September 9, 2026 08:22
@zjy365 zjy365 changed the title feat(chat): add project context discovery index feat(chat): let Project Assistant read Template README Sep 9, 2026
@norberia

norberia commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@cursoragent review this

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reviewed PR #339 and posted the review plus two inline comments. Isolation and scoping look correct; I am not requesting changes.

The tool is Project-only, schema-strict, and allowlisted from this Project’s template tasks and adopted instances. Foreign names never hit the provider, YAML/tokens never reach the model, and workspace chats do not get the tool.

Two behaviors are still worth fixing or explicitly accepting: cancel/timeout is stored as “README could not be loaded,” and the 2 MiB cap applies to the whole getTemplateSource JSON (including YAML), so a small README next to a large manifest can fail before the 32k slice. Suggested wrap of README as untrusted data because bash still auto-executes.

Walkthrough: PR #339 Template README 审阅

Open in Web Open in Cursor 

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review

Comment, not request-changes. Isolation, scoping, and “README is documentation, not live state” are in good shape. I would not block merge on security. Two behaviors are still worth fixing or explicitly accepting before this ships.

What holds

  • The tool is registered only for assistantContext.kind === "project". The schema is .strict(), so the model cannot pass projectId, namespace, or a URL. Namespace and Project come from the verified session.
  • Names are allowlisted from this Project’s deploy_tasks (source.kind = 'template') and status = 'adopted' rows, both filtered by namespace and Project id. Foreign names never hit the provider. GitHub-kind tasks, failed adoptions, and empty names stay out.
  • The fetch URL is TEMPLATE_PROVIDER_URL + /api/getTemplateSource?includeReadme=true, with redirect: "error". No model-supplied URL, no Brain-side fetch of spec.readme.
  • Only README text is returned (content, truncated, trust). appYaml / tokens are stripped. The deploy path still uses includeReadme=false.
  • 0 templates / empty README / provider throw become ordinary tool errors so other tools stay usable. ADR 0077 matches the net diff (the earlier index commit is not in the PR).

CI check / typecheck / lint is green. I did not re-run the focused bun tests in this environment (bun is not on PATH here).

Should fix, or accept in writing

1. Cancel / timeout is stored as “README could not be loaded”
createTemplateReadmeTools forwards execution.abortSignal, then catches every rejection — including AbortError from user stop and the 15s AbortSignal.timeout — and returns { ok: false, error: "Template README could not be loaded…" }. That persists as a normal tool result. Later turns treat documentation as missing instead of interrupted. The fetch path is abort-aware; swallowing abort undoes it.

Rethrow abort (error.name === "AbortError" or execution.abortSignal?.aborted). Map only provider/parse failures to { ok: false }. Same catch-all exists on catalog/OpenAPI tools, but this tool wired the signal on purpose.

2. The 2 MiB cap is the whole getTemplateSource envelope, not the README
includeReadme=true still returns YAML + source + requirements. Brain buffers that JSON, then slices README to 32k. A small README next to a large appYaml fails with "Template README response is too large.", which the catch above then turns into the generic load error. The 32k truncation never runs.

Return an explicit “provider payload too large” tool error, or spell out in ADR 0077 that 2 MiB is the entire JSON, not README bytes. Do not fetch spec.readme URLs from Brain.

Suggested, not blocking

README is raw markdown in the tool result. The description and trust: "external-documentation" say not to follow it, and it is not in the system prompt — good. The same Project toolset still auto-executes bash. Wrap content in an untrusted delimiter (ADR 0044’s <selected_resource> pattern) so a catalog README cannot look like instructions.

On an allowlist miss, return the same templates list as the unspecified-name case so the model does not need an extra round trip.

Tests

The PGlite SQL test is the one that actually proves tenant isolation (kind, status, cross-project, cross-namespace). Worth adding before this bit-rots:

  • Inverse queries: {ns-a, project-b}, {ns-b, project-a}, unknown pair, and a quote-injection-shaped namespace, expecting only that scope (or []).
  • SQL allowlist composed with a real read: adopted-only auto-read, task-only auto-read, multi-name project without listTemplateNames mocked.
  • Abort ≠ provider miss on execute.
  • README fetch asserts Authorization and cache: "no-store" the same way getTemplateSource already does.

Residual (already in the PR body / ADR)

Provider must enable README retrieval or the tool always looks empty. Returned docs are the current catalog version, not the deployed revision. GitHub-source tasks correctly never appear. Historical version matching is out of scope.

No migration, env var, public route, or approval-policy change. Direct execution on main is untouched.

Comment on lines +447 to +460
// The endpoint also returns template YAML. Bound the entire response before parsing it.
const reader = response.body.getReader();
const decoder = new TextDecoder();
let text = "";
let bytes = 0;
try {
while (true) {
const chunk = await reader.read();
if (chunk.done) {
break;
}
bytes += chunk.value.byteLength;
if (bytes > 2 * 1024 * 1024) {
throw new Error("Template README response is too large.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2 MiB bound is the whole getTemplateSource JSON (YAML + source + README), not README text.

A small README next to a large appYaml throws "Template README response is too large." before the 32k slice, and tool.ts then collapses that into the generic load error. ADR 0077 is easy to read as a README-size cap.

Either return an explicit “provider payload too large” tool error, or state in the ADR that 2 MiB is the envelope. Do not fetch spec.readme URLs from Brain to work around this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a1a2f63c. Oversize is now TemplateReadmePayloadTooLargeError (“including YAML and README”), the fixture is a short README plus a 2 MiB appYaml, and ADR 0077 states the cap is the whole provider JSON. The tool returns that message instead of the generic load failure.

Comment on lines +66 to +72
} catch {
return {
ok: false as const,
error:
"Template README could not be loaded. Continue with other tools.",
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancel/timeout is stored as a successful “could not be loaded” result.

execution.abortSignal is forwarded, then every rejection — including AbortError from user stop and the 15s AbortSignal.timeout — is mapped to { ok: false, error: "Template README could not be loaded…" }. That persists as a normal tool result, so a later turn treats documentation as missing instead of interrupted.

Rethrow abort (error.name === "AbortError" or execution.abortSignal?.aborted). Keep this catch only for provider/parse failures. The same catch-all exists on catalog/OpenAPI tools, but this tool wired the abort signal on purpose.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a1a2f63c. User cancellation now rethrows (abortSignal.aborted or AbortError). The 15s budget is a retryable timeout result, not “could not be loaded.”

Nit if you touch this again: getTemplateReadme remaps any error to timeout.reason once timeout.aborted is true, so a TooLarge that races the budget would look like a timeout. Not blocking.

@norberia

norberia commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@cursoragent review this

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Re-reviewed PR #339 at a1a2f63c. The two earlier findings are fixed; still commenting, not requesting changes.

User stop now rethrows instead of storing “could not be loaded.” The 15s budget is a retryable timeout result. Crossing 2 MiB on the whole provider JSON (short README + large YAML) returns an explicit oversize error, and ADR 0077 names the envelope. Isolation and Project-only scoping are unchanged.

One leftover nit: if the 15s signal is already aborted, getTemplateReadme remaps every error to timeout, so a TooLarge that races the budget can look like a timeout. Not a merge blocker.

Walkthrough: PR #339 Template README 审阅

Open in Web Open in Cursor 

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

Re-review (a1a2f63c)

The two items from the last pass are fixed. Still comment, not request-changes.

Cancellationexecute rethrows when execution.abortSignal is aborted or error.name === "AbortError". User stop is no longer stored as “could not be loaded.” Covered in tool.test.ts.

Timeout vs oversize — 15s timeout becomes a retryable tool result. Crossing 2 MiB on the whole getTemplateSource JSON throws TemplateReadmePayloadTooLargeError with a message that names YAML + README. The provider test uses a short README plus a 2 MiB appYaml. ADR 0077 now states the cap is the envelope.

Isolation, Project-only registration, allowlist-before-provider, and no model-supplied URL are unchanged and still look correct. CI check / typecheck / lint is green on this commit.

Remaining nit (not blocking)

In getTemplateReadme, if timeout.aborted the catch always throws timeout.reason. A TooLargeError that races the 15s budget is reported as timeout. await reader.cancel() in finally can also reject once the stream is already aborted and replace the try error; the outer remap then turns that into TimeoutError. Without an abort, cancel() fulfills and the oversized test still holds (verified on Node 22.14).

If you touch this again: swallow reader.cancel() / releaseLock(), and only remap abort-shaped errors to timeout.reason — leave TemplateReadmePayloadTooLargeError alone.

Untrusted wrapping of README body and inverse SQL tests remain suggestions, not merge conditions.

@norberia

Copy link
Copy Markdown
Contributor

@cursoragent review this

@cursor

cursor Bot commented Sep 10, 2026

Copy link
Copy Markdown

Re-reviewed PR #339 at cf5c8738. Still comment, not request-changes. README isolation is unchanged; the two earlier blockers remain fixed.

The new work after a1a2f63c needs a decision before merge. Langfuse userId is now the workspace namespace, so every member of ns-… collapses into one Langfuse user. Conversation ownership still uses global userUid, but this contradicts ADR-0056 (“associated with the owning user”). Keep userId as userUid and put namespace in metadata, or keep this grouping, attach metadata.userUid, and amend ADR-0056/0059 in this PR.

The final prompt rewrite (cf5c8738) also dropped named product-tool steering that 810f0d1a still had: readProductResourcedraftProductResourceChangewriteProductResource, and “search catalog first, even if you recognize it.” Deployment order and direct execution are still in the prompt; the model binds to tool names, so this is the bash-for-writes / skip-catalog failure mode. Restore those three bindings inside the new capability framing.

Walkthrough: PR #339 Template README 审阅

Open in Web Open in Cursor 

@cursor

cursor Bot commented Sep 10, 2026

Copy link
Copy Markdown

Re-review (cf5c8738)

Comment, not request-changes. README isolation is unchanged and the two earlier blockers remain fixed. The new work after a1a2f63c needs a decision on Langfuse, and the final prompt rewrite dropped named tool steering.

What still holds

  • readTemplateReadme is Project-only, schema-strict, and allowlisted from this Project’s deploy_tasks (source.kind = 'template') and status = 'adopted' rows. Namespace and Project come from the verified session. Foreign names never hit the provider. YAML/tokens never reach the model. Workspace chats do not get the tool.
  • User stop rethrows. The 15s budget is a retryable timeout. Crossing 2 MiB on the whole getTemplateSource JSON returns TemplateReadmePayloadTooLargeError. ADR 0077 names the envelope.
  • Conversation persist/load/billing still keys on global userUid. Quota prompt now correctly says the snapshot is workspace-wide used/limit, not Project runtime state. CI check / typecheck / lint is green.

Should resolve before merge

1. Langfuse userId is now the namespace — this contradicts ADR-0056

withLangfuseChatTrace now sets userId: owner.namespace. sessionId is still chatId. Metadata is only { feature, chatTurnId }. The owning user’s UID is gone from the trace.

ADR-0056 (2026-09-07) says Langfuse records “remain associated with the owning user and session.” CONTEXT.md still treats Assistant Conversations as personal, unlike namespace-shared layouts and free turns. Two members of ns-… become one Langfuse user. That is not a product leak to other members, but it mixes private prompts for operators and drops paid-chat cost attribution (Paid Source is the caller). “Matching Devbox scope” does not justify the whole chat turn.

No route test asserts the trace userId. The grouping exists only in the PR body and a code comment.

Fix one of: keep userId: owner.userUid and put namespace in metadata/tags; or keep namespace as userId and set metadata.userUid, and amend ADR-0056/0059 in this PR. Do not ship an undocumented ADR break.

2. cf5c8738 dropped named product-tool and catalog-recognize rules

810f0d1a / a1ef5bdf still named readProductResourcedraftProductResourceChangewriteProductResource, and “search catalog first, even if you recognize it.” HEAD says “product tools” / “sandbox commands” and “call searchDeployCatalog first” without the recognize clause or Devbox-is-not-the-app-filesystem line.

Deployment order, no invented images/secrets, GitHub source not reclassified, Project delete preview path, and direct execution are still in the prompt. Tool descriptions still carry a lot of the how. The model binds to tool names; this is the bash-for-AP/DB-write and skip-catalog failure mode. The PR body already notes the revised wording still needs a model conversation replay.

Restore the three 810f0d1a bindings inside the new capability framing.

Remaining nits (not blocking)

  • getTemplateReadme remaps any error to timeout.reason once timeout.aborted is true; reader.cancel() in finally can mask TooLargeError after abort.
  • README body is still raw markdown (trust is unused JSON). Wrap with the ADR 0044 data-block pattern if you touch this again.
  • Inverse SQL queries and Authorization/cache: "no-store" assertions on the README fetch remain suggestions.

Walkthrough: PR #339 Template README 审阅

chatTurnId,
userId: owner.userUid,
// Group telemetry by the verified workspace, matching Devbox scope.
userId: owner.namespace,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Langfuse userId is now the workspace namespace. Conversation ownership still uses userUid, but ADR-0056 says traces remain associated with the owning user. Metadata has no UID, so every member of ns-… collapses into one Langfuse user.

Keep userId: owner.userUid and put namespace in metadata/tags, or keep this grouping and add metadata.userUid plus an ADR-0056/0059 amendment. There is no route test for this value.

Comment on lines +32 to +35
"Prefer product tools for AP/DB work: read current state, draft the requested change, then apply it. Public addresses and domains belong to AP network settings. Use sandbox commands when product tools are insufficient.",
"Resolve Project targets from context or Project tools. Delete a Project only with `previewProjectDeletion` then `deleteProject`, copying preview values exactly; never delete a Project or namespace through shell commands. After deletion, refresh frontend caches and navigate away if that Project was active.",
"For named application deployments, call `searchDeployCatalog` first. Prefer a matching `template`; otherwise use `github` for a supplied repository, or `prompt` for a description. Use `docker` only for an explicitly supplied image. Ask when several Templates match, copy templateName exactly, and collect missing required args in `source.args`. Never invent image names or secrets.",
"If GitHub authentication is required, help the user connect or sign in; do not switch the repository to another source type.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

810f0d1a still named readProductResourcedraftProductResourceChangewriteProductResource and “search catalog first, even if you recognize it.” This rewrite keeps the order but drops the tool names and the recognize clause.

The model binds to tool names. Catalog and product-tool descriptions still carry the how; the system prompt is what stops bash/kubectl becoming the default write path. Restore those three 810f0d1a bindings inside this capability framing.

@zjy365
zjy365 merged commit 08031e4 into main Sep 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants