Three ways to give it the code
Project settings carry a code-source link, and the badge beside the picker says which source a scan will read. The matcher, the gap logic and the questionnaire signals are identical for all three; only where the snapshot comes from differs.
- GitHub. Uses the user's GitHub connection. This is the only source with pull-request drift checks and inbound webhooks; the UI hides those controls for the other two rather than offering a button that fails.
- GitLab, cloud or self-hosted. Instance URL plus a personal or project access token with `read_api` and `read_repository`. The token is validated against the instance before it is stored, encrypted at rest, and never returned by any API. A scan is one archive request per scan.
- An uploaded archive. A `.tar.gz`, `.tgz` or `.zip`, for an environment with no outbound git access at all. No outbound network is used.
A user-supplied GitLab instance URL goes through the same guard as a model base URL: private, loopback and link-local addresses are refused, including the cloud metadata endpoint, and a public hostname that resolves to an internal address is refused too. A self-hosted deployment is exempt, because reaching an internal instance is the whole point of one. The check runs at link time and again before every fetch, because a hostname that was public when it was linked can be repointed later.
What the scanner reads, and what it skips
The scanner reads one snapshot and matches a pattern catalogue against file text. The filters and caps are the scanner's own and apply to every source.
Skipped outright, by path: anything under `node_modules/`, `dist/`, `build/`, `.next/`, `coverage/`, `.git/`, `test/`, `tests/`, `__tests__/`, and any file whose name contains `.test.`, `.spec.`, `.min.`, or ends in `.d.ts`.
Skipped by name or type: lockfiles (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`, `Cargo.lock`, `go.sum` and the rest), known binary and media extensions, and anything that is not a recognised code file extension. A null-byte sniff on the leading bytes catches mislabelled binaries that slip past the extension check.
Caps: any single file over 512 KB is skipped, and a whole extraction is capped at 200 MB. An archive that declares less than it expands to is stopped mid-read, because bytes are counted as they arrive rather than trusted from the header. Entries stream through the matcher and are never extracted to disk, so a `../` path or a symlink inside an archive has nothing to overwrite.
One consequence worth knowing before you upload something: scan the source, not a published package. Build output is skipped, so a compiled tarball can leave nothing to read. When a scan finishes having read zero code files but skipped entries as build output, the stored summary says so rather than presenting an empty result as a clean bill of health.
Why it does not find AI everywhere
A scanner that reports AI in every repository is worthless, so the matcher is built to under-claim. Three rules do that work, each measured against real upstream packages before and after.
- Identifier anchoring. Every pattern token is word-bounded, or leading-anchored where a camelCase continuation must still match. Unanchored, case-insensitive alternations matched substrings: `ttl` inside `little`, `age` inside `image_seq_length`, `collect` inside `collections.abc`, `latitude` inside `platitude`.
- Comment and docstring masking. Line comments, block comments and Python triple-quoted strings are blanked before matching, preserving length and newlines so line numbers and snippets stay exact. A docstring that shows an import is documentation, not a deployed model. Ordinary string literals are deliberately kept, because some patterns legitimately match inference hosts that only ever appear inside strings.
- A corpus gate in CI. Small synthetic files reproduce each construct with an expectations file pinning the exact pattern ids each one may and may not produce, driven through the same functions the real scan uses. A regression fails the build rather than quietly reappearing in a customer's scan.
The automated-decision pattern also learned the vocabulary of decisions taken about people rather than generic machine-learning words, so a library full of classifiers and decision trees does not read as automated decision-making about individuals.
What leaves the server
This is the question a CISO will ask, so here is the whole answer.
- File contents never enter a prompt. Not on the GitHub path, not on the GitLab path, not on the upload path.
- What the scan stores is the finding: a pattern id, a file path, a line number, a short description and the matching line as a snippet.
- The optional scan analysis, when an AI provider is configured, receives finding titles, descriptions and file references, plus the counts and the selected frameworks. If no provider is configured, or the residency policy blocks the configured one, the scan completes anyway and records that no analysis was available.
- The optional pre-scan narrative receives the computed verdict only: the tier, the rationale statements, the flag values and their bases, and the counts of unanswered questions, detected components and prohibited-practice signals. It receives no code and no questionnaire free text.
- An uploaded archive is read once and then deleted; the findings and their file references are kept, the archive is not. The only exception is an explicit data-retention policy for evidence whose type is not delete, and the decision is logged either way.
None of this is a promise about your model provider. If you configure a hosted provider, prompt content goes to that provider from this deployment; that is a property of the provider you chose. Run a model inside your own infrastructure if AI features must make no outbound calls at all.
The code-signal prefills
A scan finding never becomes an answer. It becomes a suggestion on an unanswered question, with the file and line behind it, which a person accepts, edits or clears. A question that already has any answer is skipped, full stop, and every draft and autosave strips an unconfirmed inference so it can never reach the record as an answer nobody confirmed.
The mappings that matter for an AI Act pre-scan:
- Machine-learning framework imports, model-provider SDKs or inference hosts, and orchestration frameworks each suggest Q24 = Yes at high confidence. An imported framework in production code is the deployment of an AI system, which is what the question asks.
- Biometric recognition code suggests the Annex III biometrics area, the biometric data category and the biometric-identification data type, all at medium confidence: face-recognition code does not prove the Annex III area, since it might be an internal unlock flow.
- Emotion-detection code suggests the emotion-recognition transparency feature, at medium confidence.
- Scoring and auto-decision identifiers suggest Q30 = Yes at medium confidence: a `risk_score` identifier does not prove the decision is taken by automated means alone.
Medium-confidence signals pre-fill but never contradict, so a human answer always wins without an argument. Two of these patterns are also prohibited-practice signals in the report — face recognition and emotion detection — which is why they appear there with the file and line attached.
Provider presets and the residency policy
Project settings carry an AI panel with a provider picker. A preset pre-fills a base URL, an editable default model id and a suggested residency, and states in one line what you still have to confirm yourself. The presets cover a local server in your own infrastructure (two of them, both keyless by default and both declared local), two EU-hosted endpoints declared as EU, one US-hosted provider declared US, and two whose residency the preset cannot know and therefore leaves unspecified: an Azure resource, which is region-dependent by construction, and a routing service, whose residency is per-model and per-account.
A preset pre-fills a declaration, it never makes one. Where the answer genuinely depends on your own configuration, the preset says unspecified, which fails closed against every policy except "any provider". A base URL that still contains a placeholder is never health-checked or saved: it is not an address, and the resulting error would blame the provider for our omission.
The project's own policy is one of three: any provider, an EU-hosted or locally run provider, or a locally run provider only. Two behaviours follow, and both are enforced in code rather than documented as intent:
- The save is rejected. Landfall will not store a provider configuration that violates the project's own policy. The failure would otherwise surface days later as an unexplained error from an unrelated screen.
- The call fails closed. A blocked call returns an error naming the provider, where it runs and what the policy requires. It never quietly falls back to the platform key, and it is never bypassed by a mock mode. In the report the narrative section is then omitted entirely, with a line explaining why; in a scan, the analysis is recorded as unavailable with its reason.
A provider whose residency is undeclared satisfies only the "any provider" policy. That is deliberate: "we do not know where this runs" cannot satisfy a policy that exists to constrain where data goes.
Running a model locally
Two of the presets point at an OpenAI-compatible server running in your own infrastructure, and both are declared as local residency, so they satisfy every policy including "locally run provider only". Neither requires an API key by default. One of them serves a per-deployment catalogue, so its default model id is deliberately empty: the connection test lists what the endpoint actually has, and you pick from that.
Two practical notes. Private base URLs are only accepted on a self-hosted deployment, for the same reason a private GitLab URL is: a hosted instance must not be talked into fetching from inside someone else's network. And "local" is a claim about your own host, not something a third party verifies — the preset says so.
The self-hosting documentation carries a local-AI deployment profile in which the model runs inside the deployment, so prompts do not leave it at all. If your organisation's position is that no prompt may cross the boundary, that is the configuration to ask for, not a residency policy on a hosted provider.
The decision check and the CLI
The gate is an HTTP endpoint on the project, authenticated with an API key header. Strict mode is ON by default — it is turned off only by passing `strict=false` — so the two "warn" rows below fail the build unless you opt out. It runs several checks; the one that matters here runs only when the project actually selected the EU AI Act, and it never makes an outward provider call, because the classification needs no model.
- Undetermined risk tier: fail, listing the unanswered question keys.
- A corroborated prohibited practice: fail.
- High risk, or high risk pending the documented derogation, with an unapproved Art. 6, 27 or 49 mapping: fail, listing the mapping ids.
- Limited risk with an unapproved Art. 50 mapping: warn, and fail in strict mode.
- A claimed scope exclusion with corroborated prohibited-practice signals present: warn, and fail in strict mode.
- Otherwise: pass.
The response carries an `ai_act` block with the risk tier and the flags, each flag with its basis string. The `rtb-verify` CLI renders that block underneath the check list, so the pipeline log shows the tier, the duties and the basis for each rather than an opaque exit code. The CLI also verifies a compliance contract against the working tree, has strict, warn and audit enforcement modes, and can emit GitHub Actions annotations so failures appear inline in the pull request.
The practical sequence is short: approve the Art. 6, 27 and 49 mappings, re-run, and the gate goes green. That is the whole compliance layer between an experiment and production, expressed as an exit code.
Provisions: EU AI Act Art. 6; EU AI Act Art. 27; EU AI Act Art. 49; EU AI Act Art. 50
A short checklist
1. Confirm with your CISO which code source is acceptable, and whether an AI provider may be configured at all.
2. Link the repository, or upload an archive of the source (not a build).
3. Run the scan and read the counts: files scanned, AI components detected, questions pre-filled. Zero files scanned with skipped build output means you uploaded the wrong thing.
4. Set the project's residency policy before anyone asks for a narrative, so a violating configuration is refused at save time rather than mid-report.
5. Confirm each suggestion in the questionnaire's second stage. Nothing you do not confirm becomes an answer.
6. Wire the decision check into the pipeline once the classification obligations have been approved, so the gate has something to gate on.