Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions meta/intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,63 @@ export async function createRouxIngestIntentIdempotent(
return { intent: rowToIntent(winner[0]), created: false };
}

/**
* Atomic create-or-fetch for contextual_ingest intents keyed by the contextual
* message id (carried as payload.source.message_id, e.g. 'ctx-msg-16884').
*
* Backed by the partial unique index `cc_intents_contextual_ingest_message_id_uidx`
* (migration 0019). Distinct from createRouxIngestIntentIdempotent: the
* ON CONFLICT arbiter predicate and the re-fetch BOTH filter
* intent_type='contextual_ingest', so the conflict actually fires for
* contextual rows (the roux helper's hardcoded 'roux_ingest' predicate does
* not cover them — routing contextual through it would either never dedup or,
* with the 0019 index present, raise a unique violation).
*
* @canon: chittycanon://core/services/chittycommand/contextual-ingest
*/
export async function createContextualIngestIntentIdempotent(
env: IntentEnv,
input: CreateIntentInput & { messageId: string },
): Promise<{ intent: Intent; created: boolean }> {
const sql = getSql(env);
const initialStatus: IntentStatus =
input.sovereigntyAssessment?.decision === 'requires_human'
? 'blocked_human'
: input.sovereigntyAssessment?.decision === 'blocked'
? 'failed'
: 'pending';

const inserted = await sql`
INSERT INTO cc_intents
(plan_id, goal_id, intent_type, target_channel, payload, status, priority,
sovereignty_assessment, human_gate_reason, scheduled_for, privilege, space, metadata)
VALUES
(${input.planId}, ${input.goalId}, ${input.intentType},
${input.targetChannel ?? null}, ${JSON.stringify(input.payload)}::jsonb,
${initialStatus}, ${input.priority ?? 5},
${input.sovereigntyAssessment ? JSON.stringify(input.sovereigntyAssessment) : null}::jsonb,
${input.humanGateReason ?? null}, ${input.scheduledFor ?? null},
${input.privilege ?? 'public'}, ${input.space ?? 'business'},
${JSON.stringify(input.metadata ?? {})}::jsonb)
ON CONFLICT ((payload->'source'->>'message_id'))
WHERE intent_type = 'contextual_ingest'
AND payload->'source'->>'message_id' IS NOT NULL
DO NOTHING
RETURNING *`;
if (inserted[0]) {
return { intent: rowToIntent(inserted[0]), created: true };
}
const winner = await sql`
SELECT * FROM cc_intents
WHERE intent_type = 'contextual_ingest'
AND payload->'source'->>'message_id' = ${input.messageId}
LIMIT 1`;
if (!winner[0]) {
throw new Error(`ON CONFLICT path with no winning row for contextual message_id=${input.messageId}`);
}
return { intent: rowToIntent(winner[0]), created: false };
}

export async function getIntent(env: IntentEnv, id: string): Promise<Intent | null> {
const sql = getSql(env);
const rows = await sql`SELECT * FROM cc_intents WHERE id = ${id} LIMIT 1`;
Expand Down
47 changes: 47 additions & 0 deletions migrations/0019_contextual_provenance.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- 0019_contextual_provenance.sql
--
-- Provenance + sensitivity columns for the contextual (digested comms) upstream.
--
-- ChittyTriage intake spine (W4c): contextual → chittyrouter classify →
-- chittycommand. Every row inferred from the contextual store is AI-derived
-- and MUST be distinguishable from human/source-backed records:
-- - source = 'contextual'
-- - source_ref = the contextual message id (ctx-msg-<id>) / ChittyID
-- - confidence = classifier confidence (0..1)
-- - status = 'candidate' (NOT verified — AI/inference alone)
-- - sensitivity = 'business' | 'legalink' (per-edge Two-Space gate)
--
-- @canon: chittycanon://gov/governance#classification-axes STATUS:PENDING
-- @canon: chittycanon://core/services/chittycommand/contextual-ingest
--
-- Spine rule: a claim is `verified` only when >=1 NON-AI source backs it; AI /
-- inference alone => `candidate`. Conflicts never auto-resolve — they raise a
-- chittyagent-tasks item. See src/lib/contextual-ingest.ts.

-- ── cc_obligations: inferred bills/debts carry full provenance ──────────────
ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS source text;
ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS source_ref text;
ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS confidence numeric;
ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS sensitivity text NOT NULL DEFAULT 'business';

-- ── cc_recommendations: provenance so triage output is traceable to comms ──
ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS source text;
ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS source_ref text;
ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS sensitivity text NOT NULL DEFAULT 'business';
Comment on lines +24 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce DB-level domain constraints for confidence and sensitivity.

confidence is documented as 0..1 and sensitivity drives business vs privileged gating, but both columns currently accept invalid values. Add CHECK constraints to keep persisted state safe and enforceable at the schema boundary.

Suggested migration patch
 ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS source text;
 ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS source_ref text;
 ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS confidence numeric;
 ALTER TABLE cc_obligations ADD COLUMN IF NOT EXISTS sensitivity text NOT NULL DEFAULT 'business';
+ALTER TABLE cc_obligations
+  ADD CONSTRAINT cc_obligations_confidence_chk
+    CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1)),
+  ADD CONSTRAINT cc_obligations_sensitivity_chk
+    CHECK (sensitivity IN ('business', 'legalink'));
@@
 ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS source text;
 ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS source_ref text;
 ALTER TABLE cc_recommendations ADD COLUMN IF NOT EXISTS sensitivity text NOT NULL DEFAULT 'business';
+ALTER TABLE cc_recommendations
+  ADD CONSTRAINT cc_recommendations_sensitivity_chk
+    CHECK (sensitivity IN ('business', 'legalink'));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/0019_contextual_provenance.sql` around lines 24 - 30, The
`confidence` column in the cc_obligations table and the `sensitivity` columns in
both cc_obligations and cc_recommendations tables lack database-level
constraints to enforce valid values. Add CHECK constraints to the ALTER TABLE
statements: for the `confidence` column in cc_obligations, add a CHECK
constraint ensuring the value is between 0 and 1; for the `sensitivity` columns
in both tables, add a CHECK constraint that restricts values to valid domain
options (such as 'business' and 'privileged'). These constraints should be added
as part of the respective ALTER TABLE ADD COLUMN statements or in separate ALTER
TABLE ADD CONSTRAINT statements to ensure data integrity at the schema boundary.


-- ── Dedup guard: one inferred obligation per contextual source_ref ─────────
-- Partial so it only constrains contextual-sourced rows; other upstreams
-- (quo, email_bills, manual) are unaffected.
CREATE UNIQUE INDEX IF NOT EXISTS cc_obligations_contextual_source_ref_uidx
ON cc_obligations (source_ref)
WHERE source = 'contextual' AND source_ref IS NOT NULL;

-- ── Idempotency for contextual_ingest intents (mirrors 0017 roux_ingest) ───
-- Backs INSERT ... ON CONFLICT DO NOTHING so concurrent ingest runs of the
-- same contextual message collapse to one intent.
CREATE UNIQUE INDEX IF NOT EXISTS cc_intents_contextual_ingest_message_id_uidx
ON cc_intents ((payload->'source'->>'message_id'))
WHERE intent_type = 'contextual_ingest'
AND payload->'source'->>'message_id' IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_cc_obligations_source ON cc_obligations (source, status);
109 changes: 109 additions & 0 deletions scripts/verify-contextual-ingest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Real verification harness for the contextual → triage ingest.
*
* Runs the ACTUAL module (src/lib/contextual-ingest.ts) against:
* - the contextual store (Neon delicate-moon, read) via CONTEXTUAL_DATABASE_URL
* - a cool-bar-13270800 BRANCH (write) via DATABASE_URL
*
* The chittyagent-tasks prod token is gated (POLICY_BLOCKED_CHITTYCONNECT_
* UNAVAILABLE), so for the conflict→task proof we stand up a LOCAL instance of
* the real /api/v1/tasks endpoint, backed by the SAME branch DB
* (agent_tasks.tasks — chittyagent-tasks shares cool-bar's Hyperdrive). The
* integrations.tasksClient HTTP call therefore exercises the real client and
* writes a REAL row into the real agent_tasks.tasks schema on the branch.
*
* No mocks: real SQL, real schema, real classifier path (router /process is
* 404 in prod → deterministic feature fallback, which is real inference, not a
* stub). env.AI is absent outside the Worker runtime, so the AI branch is
* skipped and the deterministic fallback is used — recorded in classifier_via.
*
* Usage: node scripts/verify-contextual-ingest.mjs
* requires env: DATABASE_URL, CONTEXTUAL_DATABASE_URL
*/
import http from 'node:http';
import { neon } from '@neondatabase/serverless';
import { ingestContextual } from '../src/lib/contextual-ingest.ts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how this script is expected to run and whether TS runtime support is configured.
node -v
rg -n -C3 "verify-contextual-ingest|tsx|ts-node|--loader|--import|experimental-strip-types" package.json || true
rg -n -C3 "Usage: node scripts/verify-contextual-ingest\\.mjs|import \\{ ingestContextual \\} from '../src/lib/contextual-ingest\\.ts'" scripts/verify-contextual-ingest.mjs

Repository: chittyos/chittycommand

Length of output: 628


🏁 Script executed:

cat package.json | head -100

Repository: chittyos/chittycommand

Length of output: 1420


🏁 Script executed:

find . -maxdepth 2 -name "tsconfig.json" -o -name "tsconfig*.json" | head -20

Repository: chittyos/chittycommand

Length of output: 101


🏁 Script executed:

rg -n "build|compile|scripts" package.json -A 5 -B 1 | head -50

Repository: chittyos/chittycommand

Length of output: 863


🏁 Script executed:

ls -la src/lib/contextual-ingest.* 2>/dev/null || echo "No file found"

Repository: chittyos/chittycommand

Length of output: 145


🏁 Script executed:

find . -maxdepth 2 -name "wrangler.toml" -o -name "wrangler.json" | xargs cat 2>/dev/null | head -50

Repository: chittyos/chittycommand

Length of output: 158


🏁 Script executed:

ls -la dist/ 2>/dev/null || echo "No dist directory"

Repository: chittyos/chittycommand

Length of output: 84


🏁 Script executed:

ls -la build/ 2>/dev/null || echo "No build directory"

Repository: chittyos/chittycommand

Length of output: 85


🏁 Script executed:

cat tsconfig.json | head -50

Repository: chittyos/chittycommand

Length of output: 636


🏁 Script executed:

cat -n scripts/verify-contextual-ingest.mjs | head -40

Repository: chittyos/chittycommand

Length of output: 2260


🏁 Script executed:

grep -r "verify-contextual-ingest" . --include="*.md" --include="*.json" --include="*.sh" 2>/dev/null | head -20

Repository: chittyos/chittycommand

Length of output: 48


🏁 Script executed:

# Check if there's a compiled JavaScript version of the TypeScript file anywhere
find . -name "contextual-ingest.js" -o -name "contextual-ingest.mjs" 2>/dev/null

Repository: chittyos/chittycommand

Length of output: 48


🏁 Script executed:

# Check if there's any build process that might compile scripts directory
rg -n "scripts/" package.json wrangler.toml 2>/dev/null || true

Repository: chittyos/chittycommand

Length of output: 145


🏁 Script executed:

# Check if there's any loader or flag configuration in wrangler.toml or environment
cat wrangler.toml

Repository: chittyos/chittycommand

Length of output: 112


🏁 Script executed:

# Check if there's a compiled version in the scripts directory or elsewhere
find . -type f -name "*.mjs" -o -name "*.js" | grep -E "(scripts|src/lib)" | head -20

Repository: chittyos/chittycommand

Length of output: 105


🏁 Script executed:

# Check what Node.js experimental features might be enabled
grep -r "experimental" . --include="package.json" --include="wrangler.toml" --include=".npmrc" 2>/dev/null || echo "No experimental config found"

Repository: chittyos/chittycommand

Length of output: 95


🏁 Script executed:

# Verify if this script is actually meant to be run with node or with a different tool
file scripts/verify-contextual-ingest.mjs

Repository: chittyos/chittycommand

Length of output: 111


Importing .ts directly from .mjs will fail under the documented invocation.

Line 25 imports ../src/lib/contextual-ingest.ts, but the script's documentation specifies Usage: node scripts/verify-contextual-ingest.mjs without any TypeScript tooling. The project's build setup confirms this:

  • tsconfig.json explicitly excludes the scripts/ directory and has noEmit: true (type-check only)
  • No compiled .js version of contextual-ingest.ts exists
  • No TypeScript loader is configured (no --loader, --experimental-strip-types, or tsx)

Running the script with plain node will fail at the import statement. Either wire a TypeScript loader (via tsx, ts-node, or Node's --experimental-strip-types flag with Node 18.19+), or point the import to a compiled .js file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-contextual-ingest.mjs` at line 25, The import statement in
verify-contextual-ingest.mjs imports a TypeScript file directly, but the script
is documented to run with plain node without any TypeScript tooling configured.
Either change the import statement at line 25 to reference a compiled .js
version of contextual-ingest instead of the .ts file, or configure a TypeScript
loader (such as tsx, ts-node, or Node's --experimental-strip-types flag) to
enable the script to load TypeScript modules at runtime. Ensure the chosen
approach aligns with the project's build setup and the documented invocation
method.


const BRANCH_URL = process.env.DATABASE_URL;
const CTX_URL = process.env.CONTEXTUAL_DATABASE_URL;
if (!BRANCH_URL || !CTX_URL) {
console.error('Set DATABASE_URL (branch) and CONTEXTUAL_DATABASE_URL (contextual)');
process.exit(1);
}

const LOCAL_TASKS_TOKEN = 'verify-harness-local-token';
const sql = neon(BRANCH_URL);

// Local stand-in for chittyagent-tasks POST /api/v1/tasks, backed by the SAME
// branch DB (real agent_tasks.tasks schema). This is the real createTask INSERT
// from chittyentity/workers/shared/agent-tasks.ts.
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || !req.url.endsWith('/api/v1/tasks')) {
res.writeHead(404); return res.end('not found');
}
if (req.headers['authorization'] !== `Bearer ${LOCAL_TASKS_TOKEN}`) {
res.writeHead(403); return res.end(JSON.stringify({ success: false, error: 'Invalid token' }));
}
let body = '';
req.on('data', (c) => (body += c));
req.on('end', async () => {
try {
const i = JSON.parse(body);
const [row] = await sql`
INSERT INTO agent_tasks.tasks
(title, description, task_type, assigned_agent, source_agent, status, priority,
payload, depends_on, triage_class, urgency, needs_nick, notify_policy)
VALUES
(${i.title}, ${i.description ?? null}, ${i.task_type}, ${i.assigned_agent},
${i.source_agent ?? 'chittycommand'}, 'pending', ${i.priority ?? 5},
${JSON.stringify(i.payload ?? {})}, ${[]}, ${i.triage_class ?? null},
${i.urgency ?? null}, ${i.needs_nick ?? false}, 'done_only')
RETURNING id, title, assigned_agent, status`;
res.writeHead(201, { 'content-type': 'application/json' });
res.end(JSON.stringify({ success: true, data: row }));
} catch (err) {
console.error('[local-tasks] insert failed:', err);
res.writeHead(500); res.end(JSON.stringify({ success: false, error: String(err) }));

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid returning raw exception text in HTTP error responses.

Line 66 exposes String(err) to the client response. Keep details in server logs and return a generic error payload instead.

🔒 Suggested fix
-      res.writeHead(500); res.end(JSON.stringify({ success: false, error: String(err) }));
+      res.writeHead(500, { 'content-type': 'application/json' });
+      res.end(JSON.stringify({ success: false, error: 'internal_error' }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.writeHead(500); res.end(JSON.stringify({ success: false, error: String(err) }));
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'internal_error' }));
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 66-66: Exception text reinterpreted as HTML
Exception text is reinterpreted as HTML without escaping meta-characters.


[warning] 66-66: Information exposure through a stack trace
This information exposed to the user depends on stack trace information.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-contextual-ingest.mjs` at line 66, The HTTP error response at
the 500 status code handler is exposing raw exception details by including
`String(err)` directly in the client response, which is a security
vulnerability. Replace the `error: String(err)` in the JSON response with a
generic error message like `error: "Internal server error"`, and instead log the
actual error details (using `String(err)` or similar) to the server logs for
debugging purposes. This way, sensitive error information stays on the server
while clients receive a safe, generic response.

Source: Linters/SAST tools

}
});
});

await new Promise((r) => server.listen(0, r));
const port = server.address().port;

const env = {
DATABASE_URL: BRANCH_URL,
CONTEXTUAL_DATABASE_URL: CTX_URL,
// router /process is 404 in prod; classifier falls through to deterministic
// feature path (real). Set the real URL so the attempt is genuinely made.
CHITTYROUTER_URL: 'https://router.chitty.cc',
CHITTYAGENT_TASKS_URL: `http://127.0.0.1:${port}`,
CHITTYAGENT_TASKS_TOKEN: LOCAL_TASKS_TOKEN,
COMMAND_KV: { get: async () => null, put: async () => {}, delete: async () => {} },
// no AI binding outside the worker runtime
};

const before = (await sql`SELECT
(SELECT count(*) FROM cc_intents WHERE intent_type='contextual_ingest') intents,
(SELECT count(*) FROM cc_obligations WHERE source='contextual') obligations,
(SELECT count(*) FROM cc_recommendations WHERE source='contextual') recs,
(SELECT count(*) FROM agent_tasks.tasks WHERE source_agent='chittycommand' AND task_type='reconciliation') tasks`)[0];

const result = await ingestContextual(env, sql, { limit: 50 });

const after = (await sql`SELECT
(SELECT count(*) FROM cc_intents WHERE intent_type='contextual_ingest') intents,
(SELECT count(*) FROM cc_obligations WHERE source='contextual') obligations,
(SELECT count(*) FROM cc_recommendations WHERE source='contextual') recs,
(SELECT count(*) FROM agent_tasks.tasks WHERE source_agent='chittycommand' AND task_type='reconciliation') tasks`)[0];

console.log('=== ingest result ===');
console.log(JSON.stringify(result, null, 2));
console.log('=== before → after ===');
console.log('cc_intents(contextual_ingest):', before.intents, '→', after.intents);
console.log('cc_obligations(contextual): ', before.obligations, '→', after.obligations);
console.log('cc_recommendations(contextual):', before.recs, '→', after.recs);
console.log('agent_tasks.tasks(reconciliation/chittycommand):', before.tasks, '→', after.tasks);

server.close();
process.exit(0);
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export type Env = {
CHITTYGOV_URL?: string;
CHITTYGOV_TOKEN?: string;
CHITTYAGENT_SCRAPE_URL?: string;
// chittyagent-tasks (canonical distributed task queue) — conflict escalation
CHITTYAGENT_TASKS_URL?: string;
CHITTYAGENT_TASKS_TOKEN?: string;
// Contextual (digested cross-channel comms) read connection.
// Separate Neon project (ChittyLedger-Messaging) — NOT the command DB.
// @canon: contextual-store-neon-location
CONTEXTUAL_DATABASE_URL?: string;
CHITTYREGISTER_URL?: string;
CHITTYCHAT_DATA_API?: string;
CHITTYSCHEMA_URL?: string;
Expand Down
Loading
Loading