Repository navigation
fix(audit): harden workspace, deployment handoffs and chat boundaries #2378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b7098f1
fix(audit): restore hybrid helper runtime and document current migrat…
groupthinking fdab33b
fix(deployment): reject failed git stages and unverified handoff iden…
groupthinking a46b5fe
fix(chat): reject malformed pack bindings before identity construction
groupthinking b5e80fd
test(hybrid): verify terminal effects and tokenize actual asset attri…
groupthinking 8862ea9
fix(hybrid): handle whitespace in browser script closing tags
groupthinking 10f440c
Merge current main to include verified canonical video source boundary
groupthinking a85ccb8
fix(hybrid): use native HTML parsing for virtual asset substitution
groupthinking File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| name: Hybrid Workspace Boundary | ||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'apps/hybrid-workspace/**' | ||
| - '.github/workflows/hybrid-workspace-boundary.yml' | ||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| browser: | ||
| name: Hybrid workspace browser boundary | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| env: | ||
| VERIFIED_HEAD: ${{ github.event.pull_request.head.sha }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '24' | ||
| - run: npm ci --ignore-scripts | ||
| - run: node apps/hybrid-workspace/logic.test.mjs | ||
| - run: cd apps/web && npx --no-install playwright install --with-deps chromium | ||
| - run: node apps/hybrid-workspace/browser.test.mjs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // Browser plugin not available. Uses Playwright Chromium and actual app modules, | ||
| // with fixture adapters for CDN editor/terminal/layout APIs; no live Gateway. | ||
| import { chromium } from 'playwright'; | ||
| import { createServer } from 'node:http'; | ||
| import { readFile } from 'node:fs/promises'; | ||
| import assert from 'node:assert/strict'; | ||
| const root = new URL('./', import.meta.url); | ||
| const allowed = new Set(['index.html','app.js','logic.js','fx-adapter.js','styles.css']); | ||
| const server = createServer(async (req,res) => { | ||
| const name = req.url === '/' ? 'index.html' : req.url.slice(1); | ||
| if (!allowed.has(name)) { res.writeHead(404).end(); return; } | ||
| res.setHeader('Content-Type', name.endsWith('.js') ? 'text/javascript' : name.endsWith('.css') ? 'text/css' : 'text/html'); | ||
| res.end(await readFile(new URL(name,root))); | ||
| }); | ||
| await new Promise(r => server.listen(0,'127.0.0.1',r)); | ||
| let browser; | ||
| try { | ||
| browser = await chromium.launch({headless:true}); | ||
| const page = await browser.newPage(); | ||
| const origin = `http://127.0.0.1:${server.address().port}`; | ||
| const errors = []; | ||
| page.on('pageerror', e => errors.push(e.message)); | ||
| await page.route('**/*', route => route.request().url().startsWith(origin+'/') ? route.continue() : route.fulfill({status:200,body:'',contentType:'text/javascript'})); | ||
| await page.addInitScript(() => { | ||
| window.Split = () => {}; | ||
| const models = [], changes = []; | ||
| let active; | ||
| window.monaco = {editor:{createModel(value) {const m={getValue:()=>value,getValueInRange:()=>value,edit:v=>{value=v;}}; models.push(m);return m;},create(_el,opts){active=opts.model;return {setModel:m=>{active=m;},getModel:()=>active,getSelection:()=>({}),onDidChangeModelContent:fn=>changes.push(fn)};}}}; | ||
| window.require = (_deps,fn) => fn(); window.require.config = () => {}; | ||
| window.fixtureEdit = (index,value) => {active=models[index];active.edit(value);changes.forEach(fn=>fn());}; | ||
| window.Terminal = class {open(){} write(){} writeln(){} onData(fn){window.fixtureType=fn;}}; | ||
| Object.defineProperty(window,'AI_GATEWAY_API_KEY',{get(){throw new Error('Forbidden credential access');},set(){throw new Error('Forbidden credential write');}}); | ||
| }); | ||
| await page.goto(origin); | ||
| await page.waitForFunction(() => document.querySelectorAll('#files button').length===4); | ||
| assert.equal(await page.title(),'UVAI Hybrid Workspace'); | ||
| assert.equal(await page.locator('#preview').getAttribute('sandbox'),'allow-scripts'); | ||
| await page.locator('[data-action="embed"]').click(); | ||
| assert.match(await page.locator('#thread').innerText(),/Split screen stays locked/); | ||
| await page.evaluate(() => window.fixtureEdit(0,'<title>Edited</title><h1>New</h1><script>try { parent.document.body.dataset.poison="1" } catch { document.body.dataset.confined="1" }</script>')); | ||
| await page.locator('[data-action="summarize"]').click(); | ||
| assert.match(await page.locator('#thread').innerText(),/Edited/); | ||
| assert.equal(await page.locator('body').getAttribute('data-poison'),null); | ||
| const preview = page.frameLocator('#preview'); | ||
| await preview.locator('body[data-confined="1"]').waitFor(); | ||
| await page.evaluate(() => window.fixtureEdit(3,'<img src=x onerror="parent.document.body.dataset.poison=1">')); | ||
| await page.locator('[data-action="summarize"]').click(); | ||
| assert.equal(await page.locator('#thread img').count(),0); | ||
| await page.locator('[data-action="extract"]').click(); | ||
| assert.equal(await page.locator('#thread img').count(),0); | ||
| await page.locator('#ask').fill('<img onerror="x">'); | ||
| await page.locator('#composer button').click(); | ||
| assert.equal(await page.locator('#thread img').count(),0); | ||
| await page.locator('[data-action="escalate"]').click(); | ||
| const ticketsBefore = await page.locator('#thread .ticket').count(); | ||
| await page.evaluate(() => window.fixtureType('ask file a ticket\r')); | ||
| assert.equal(await page.locator('#thread .ticket').count(), ticketsBefore + 1); | ||
| assert.match(await page.locator('#thread').innerText(),/session only/); | ||
| assert.deepEqual(errors,[]); | ||
| console.log(JSON.stringify({status:'VERIFIED',head:process.env.VERIFIED_HEAD || 'local-unpinned',surface:'hybrid-workspace',boundary:'Chromium DOM + sandbox',externalServices:'disabled',editor:'fixture adapter',checks:['identity','embed','edited summary','extract','ask escaping','terminal escalation','sandbox confinement','credential access trap','console health']})); | ||
| } finally { await browser?.close(); await new Promise(r=>server.close(r)); } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
apps/web/src/lib/__tests__/chat-pack-binding-boundary.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { expect, it, vi } from 'vitest'; | ||
| vi.mock('@/lib/video-pack', () => ({ buildIdentityPack: vi.fn(() => { throw new Error('Identity builder must not run for malformed input'); }), isIdentityOnlyPack: vi.fn() })); | ||
| vi.mock('@/lib/video-pack-store', () => ({ getPackRecordWithMeta: vi.fn() })); | ||
| import { buildIdentityPack } from '@/lib/video-pack'; | ||
| import { parseChatPackBinding, resolveChatPackGrounding } from '@/lib/chat-pack-grounding'; | ||
| it('rejects malformed video and pack identifiers before constructing identity', () => { | ||
| for (const video_id of ['bad', 'auJzb1D-fag-extra', 'https://youtu.be/auJzb1D-fag']) { | ||
| expect(parseChatPackBinding({video_id})).toBeNull(); | ||
| expect(parseChatPackBinding({pack_id:`vp:v0:${video_id}`})).toBeNull(); | ||
| } | ||
| }); | ||
| it('rejects directly supplied malformed bindings with400 and no identity lookup', async () => { | ||
| expect(await resolveChatPackGrounding({videoId:'bad',packId:'vp:v0:bad'})).toMatchObject({ok:false,status:400,code:'pack_id_mismatch'}); | ||
| expect(buildIdentityPack).not.toHaveBeenCalled(); | ||
| }); | ||
| it('accepts only matching canonical identifiers', () => { | ||
| expect(parseChatPackBinding({pack_id:'vp:v0:auJzb1D-fag'})).toEqual({videoId:'auJzb1D-fag',packId:'vp:v0:auJzb1D-fag'}); | ||
| expect(parseChatPackBinding({video_id:'auJzb1D-fag',pack_id:'vp:v0:jNQXAC9IVRw'})).toBeNull(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,15 @@ | ||
| # SQL model vs migration truth (AUDIT-007 / #2169) | ||
| # SQL model and migration truth (AUDIT-007 / #2169) | ||
|
|
||
| ## Current state | ||
| ## Current repository evidence — 2026-10-08 | ||
|
|
||
| | Area | Location | Notes | | ||
| | --- | --- | --- | | ||
| | Model mixins | `src/youtube_extension/backend/models/base.py` | Timestamps, tenant, UUID, soft-delete | | ||
| | Model modules | `src/youtube_extension/backend/models/*.py` | Multiple domain modules | | ||
| | Alembic revisions | `src/youtube_extension/backend/migrations/versions/` | **3** committed revisions (`001`–`003`) | | ||
| The previous inventory in this file named backend model modules and three Alembic revisions. That inventory is obsolete: the current recursive repository tree does not contain `src/youtube_extension/backend/models/`, `src/youtube_extension/backend/migrations/`, or `tests/unit/test_migration_truth.py`. `alembic.ini` still points to the missing migration directory. Do not treat the old count guard or directory list as verified current coverage. | ||
|
|
||
| ## Policy | ||
| One SQL migration is present at `apps/web/supabase/migrations/20260912214530_create_private_studio_chat_ownership.sql`. Its presence alone does not prove that all live tables, tenant policies, deployed revisions, or Python persistence models are reconciled. | ||
|
|
||
| 1. Any model used in production request paths must have a matching migration revision or an explicit “non-prod / optional store” doc entry here. | ||
| 2. New tables/columns require Alembic revision in the same PR as model changes. | ||
| 3. CI guard: `tests/unit/test_migration_truth.py` fails if revision count drops or model package disappears. | ||
| ## Acceptance and blocking requirements | ||
|
|
||
| ## Follow-up (tracked) | ||
| Issue #2169 remains open. Before creating migrations, identify the actual active database and owning service, inventory current persistence models and deployed revision/RLS state using approved read-only access, and map each model to its authoritative migration history. The missing legacy directory needs an explicit retirement or restoration decision; do not silently manufacture a replacement migration chain. | ||
|
|
||
| Generate incremental migrations for unmigrated model domains after inventory sign-off. Do not auto-generate destructive migrations in CI. | ||
| No live schema comparison, database access, migration generation, or migration application was performed in this correction. A destructive migration requires separate approval. New tables/columns must accompany the matching migration and fixture validation in the same PR. | ||
|
|
||
| Historical claim retained for audit: the previous file asserted three committed revisions (`001`–`003`) and a `test_migration_truth.py` guard. That claim is not valid for the inspected current tree. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.