Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2360Done

(motir-ai) `GET /v1/jobs/:id` and its stream become TENANT-SCOPED — a job that is not the caller's project's is a 404

The producer half of the job-ownership boundary contract. One PR in motir-ai; its consumer half is ai:plan (4/4) in motir-core, which lands FIRST and starts sending the core project id.

What is on origin/main (a01d96b). src/app.ts registers app.get('/v1/jobs/:id', serviceAuth, …) answering getJobView(c.req.param('id')), and src/jobs/planJobService.ts's getJobView(id) is planJobRepository.findById(db, id) with no tenant predicate. The stream route beside it is the same. The binding is right there and unused: PlanJob.aiProjectIdAiProject, which submitJob resolves through findOrCreateByCoreIds({ coreOrganizationId, coreWorkspaceId, coreProjectId }). So the job knows whose it is, and the read does not ask.

The convention to follow is in the same file. GET /v1/usage already requires the core ids per scope — its own comment says "the scope's coreWorkspaceId/coreProjectId are required per level so a foreign id can't widen the rollup" — and /v1/preplan requires coreProjectId outright. This card applies the same posture to the two job reads, which are the only /v1 endpoints that take an opaque id and answer without one.

The shape

  • Both routes require a coreProjectId query parameter, validated the way the neighbouring routes validate theirs (requireQuery).
  • The lookup resolves the job's AiProject and compares coreProjectId. On a mismatch it raises the same not_found ApiError as an unknown id — a foreign job and a missing job must be indistinguishable from outside, or the endpoint becomes an existence oracle for other tenants' job ids.
  • The stream applies the check ONCE before the first frame, not per poll.
  • Nothing about POST /v1/jobs changes: it already carries the tenant envelope.

Acceptance criteria

  • GET /v1/jobs/:id and GET /v1/jobs/:id/stream both require coreProjectId and reject a request without it with the same validation_error shape the sibling routes use.
  • A job whose AiProject.coreProjectId differs from the query's returns not_found with a body byte-identical in shape to the unknown-id case, asserted by a test that compares the two responses rather than each separately.
  • The stream refuses before emitting any frame, proved by a test that asserts on an empty body.
  • getJobView's signature carries the scoping argument, so a future caller cannot get an unscoped read by accident — there is no unscoped overload left in the module.
  • Any document in this repo describing the §2.4 job-view contract is updated in the same PR; if grep -rn "2.4" src docs finds none, say so in the PR body rather than leaving it ambiguous.
  • The existing job-lifecycle tests still pass with the new required parameter, and the suite is green.

Context refs

  • src/app.ts — the two /v1/jobs/:id* route registrations, and /v1/usage + /v1/preplan as the convention to copy.
  • src/jobs/planJobService.tsgetJobView, toJobView, submitJob and its findOrCreateByCoreIds tenant resolution.
  • src/jobs/planJobRepository.tsfindById, the lookup that needs the predicate.
  • prisma/schema.prismaPlanJob.aiProjectId and the AiProject core-id columns.
  • ai:plan (4/4) — the consumer half, which must be merged before this one.